# Netzo Map — API & Integration Guide

> 🤖 **For AI Agents & Developers:** This is the primary technical document for integrating the Netzo self-hosted Iranian vector map server (`https://map.netzo.ir/`) into any web application or frontend framework. No prior GIS knowledge is required.

---

## ⚡ 0. AI Agent 10-Second Quickstart

If you are an AI agent instructed to embed a map in a project, **copy-paste this snippet directly into an HTML file or iframe**. It works out of the box with zero external dependencies besides standard MapLibre CDN:

```html
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>نقشه نتزو</title>
  <link href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet" />
  <style>
    html, body, #map { margin: 0; padding: 0; width: 100%; height: 100%; }
  </style>
</head>
<body>
<div id="map"></div>
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
<script>
  // 1. Mandatory RTL plugin for proper Persian cursive shaping
  maplibregl.setRTLTextPlugin(
    'https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.2.3/mapbox-gl-rtl-text.min.js',
    null,
    true
  );

  // 2. Initialize map using the Netzo production vector style
  const map = new maplibregl.Map({
    container: 'map',
    style: 'https://map.netzo.ir/styles/iran-persian.json',
    center: [53.6880, 32.4279], // [longitude, latitude] — Geographic center of Iran
    zoom: 5.5,
    minZoom: 4,
    maxZoom: 18
  });

  // 3. Add zoom & rotation controls
  map.addControl(new maplibregl.NavigationControl(), 'top-left');
</script>
</body>
</html>
```

> **Base URL:** `https://map.netzo.ir`  
> If using a custom self-hosted instance, change `https://map.netzo.ir` to your server's URL.  
> All endpoints have CORS enabled (`Access-Control-Allow-Origin: *`).

---

## Table of Contents

0. [AI Agent 10-Second Quickstart](#-0-ai-agent-10-second-quickstart)
1. [Base URL & Endpoints Overview](#1-base-url--endpoints-overview)
2. [Map Tile & Style API](#2-map-tile--style-api)
3. [Location Pinning API (Client-Side)](#3-location-pinning-api-client-side)
4. [Location Search & Forward Geocoding API](#4-location-search--forward-geocoding-api)
5. [Reverse Geocoding API (Coordinates to Address)](#5-reverse-geocoding-api-coordinates-to-address)
6. [Complete Combined Example (Search + Click + Reverse + Pin)](#6-complete-combined-example-search--click--reverse--pin)
7. [React / Next.js / Vue Integration](#7-react--nextjs--vue-integration)
8. [Error & Troubleshooting Reference](#8-error--troubleshooting-reference)
9. [Coordinate System Reference](#9-coordinate-system-reference)

---

## 1. Base URL & Endpoints Overview

| Endpoint | Method | Purpose |
| :--- | :--- | :--- |
| `https://map.netzo.ir/styles/iran-persian.json` | `GET` | Main MapLibre style definition with Persian font layers |
| `https://map.netzo.ir/tiles/iran/{z}/{x}/{y}` | `GET` | Vector tiles (PBF) served by Martin from PMTiles |
| `https://map.netzo.ir/raster/iran/{z}/{x}/{y}.png` | `GET` | **Raster tiles (PNG)** standard XYZ for Leaflet, OpenLayers & GIS |
| `https://map.netzo.ir/style/iran-persian/static/{lon},{lat},{zoom}/{w}x{h}.png` | `GET` | **Static map images** on-the-fly rendering |
| `https://map.netzo.ir/fonts/{fontstack}/{range}.pbf` | `GET` | Pre-sliced Vazirmatn Persian SDF font glyphs |
| `https://map.netzo.ir/sprites/sprite[.png/.json]` | `GET` | Map icon sprite atlas |
| `https://map.netzo.ir/search?q={query}` | `GET` | Persian place search & geocoding (GeoJSON output) |
| `https://map.netzo.ir/api/reverse-geocode?lat={lat}&lng={lng}` | `GET` | Reverse geocoding: coordinates to structured address JSON |
| `https://map.netzo.ir/ping` | `GET` | Health check endpoint (returns `pong`) |

---

## 2. Map Tile & Style API

### 2.1 Vector vs. Raster: Which should you choose?

| Feature | Vector Tiles (`/tiles/iran/...`) | Raster Tiles (`/raster/iran/...`) |
| :--- | :--- | :--- |
| **Recommended Library** | **MapLibre GL JS**, Mapbox GL, MapLibre Native | **Leaflet**, OpenLayers, QGIS, Flutter/RN Leaflet |
| **Rendering Mechanism** | Client-side GPU (WebGL / WebGPU) | Server-side rendered PNG/WebP images |
| **Resolution & Sharpness** | Crisp at any DPI / Retina / Zoom level | Fixed 256×256 px image tiles |
| **Map Rotation & 3D Pitch** | ✅ Full 360° rotation & 60° 3D tilt | ❌ Fixed 2D top-down view |
| **Client Resource Usage** | Moderate GPU usage | Minimal CPU & RAM (lightweight) |
| **Best For** | Modern web apps, interactive navigations | Simple widgets, older mobile devices, GIS apps |

---

### 2.2 Style Specification (Vector)

```
GET /styles/iran-persian.json
```

Returns the full MapLibre Style Specification JSON. Pass this URL as the `style` property in `new maplibregl.Map({ style: "..." })`.

---

### 2.3 Vector Tile Fetch (PBF)

```
GET /tiles/iran/{z}/{x}/{y}
```

Returns a single binary vector tile in Mapbox Vector Tile (`.pbf`) format.

| Parameter | Type | Description |
| :--- | :--- | :--- |
| `z` | integer | Zoom level (0–14) |
| `x` | integer | Tile column (XYZ scheme) |
| `y` | integer | Tile row (XYZ scheme) |

**Response:** `200 OK`, `Content-Type: application/x-protobuf`  
**Cache:** Cached by Nginx in high-speed tile cache.

---

### 2.4 Raster Tile API (XYZ Standard)

```
# Light Mode:
GET /raster/light/{z}/{x}/{y}.png
GET /raster/iran/{z}/{x}/{y}.png

# Dark Mode:
GET /raster/dark/{z}/{x}/{y}.png
GET /raster/iran/dark/{z}/{x}/{y}.png
```

Returns a pre-rendered 256×256 PNG raster tile in light or dark theme. Fully compatible with any mapping client supporting standard Slippy Map / XYZ URL templates.

#### Leaflet Integration Example:
```html
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<div id="map" style="width: 100%; height: 500px;"></div>

<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
  const map = L.map('map').setView([35.6892, 51.3890], 12); // Tehran

  L.tileLayer('https://map.netzo.ir/raster/iran/{z}/{x}/{y}.png', {
    maxZoom: 18,
    minZoom: 4,
    attribution: '© Netzo Map | OpenStreetMap contributors'
  }).addTo(map);

  L.marker([35.6892, 51.3890]).addTo(map)
    .bindPopup('تهران - میدان آزادی')
    .openPopup();
</script>
```

#### OpenLayers Integration Example:
```javascript
import Map from 'ol/Map';
import View from 'ol/View';
import TileLayer from 'ol/layer/Tile';
import XYZ from 'ol/source/XYZ';
import { fromLonLat } from 'ol/proj';

const map = new Map({
  target: 'map',
  layers: [
    new TileLayer({
      source: new XYZ({
        url: 'https://map.netzo.ir/raster/iran/{z}/{x}/{y}.png'
      })
    })
  ],
  view: new View({
    center: fromLonLat([53.6880, 32.4279]),
    zoom: 6
  })
});
```

---

### 2.5 Static Map Image API

Generate static image snapshots for thumbnails, SMS links, reports, or social media previews:

```
GET /style/iran-persian/static/{longitude},{latitude},{zoom}/{width}x{height}.png
```

| Parameter | Type | Example | Description |
| :--- | :--- | :--- | :--- |
| `longitude` | float | `51.3890` | Center longitude |
| `latitude` | float | `35.6892` | Center latitude |
| `zoom` | float | `13` | Zoom level |
| `width` | integer | `600` | Image width in pixels |
| `height` | integer | `400` | Image height in pixels |

**Example URL:**
```html
<img src="https://map.netzo.ir/style/iran-persian/static/51.3890,35.6892,13/600x400.png" alt="Map Preview" />
```

---

### 2.6 Font Glyphs (Persian SDF)

```
GET /fonts/{fontstack}/{range}.pbf
```

Returns Signed Distance Field (SDF) font glyph data for rendering Persian text on WebGL canvas.

**Example:**
```
GET /fonts/Vazirmatn%20Regular/0-255.pbf
GET /fonts/Vazirmatn%20Regular/1280-1535.pbf  ← Persian Unicode block
```

---

### 2.5 Sprites (Icons)

```
GET /sprites/sprite.json
GET /sprites/sprite.png
GET /sprites/sprite@2x.json
GET /sprites/sprite@2x.png
```

Icon sprite sheet consumed automatically by MapLibre.

---

## 3. Location Pinning API (Client-Side)

Pinning is performed client-side using MapLibre JS markers.

### 3.1 Add a Single Pin

```javascript
// Order is ALWAYS [longitude, latitude]
const marker = new maplibregl.Marker({
  color: '#e53e3e',
  draggable: false
})
  .setLngLat([51.3890, 35.6892])  // Tehran: [lng, lat]
  .addTo(map);
```

> ⚠️ **Important:** Coordinate order is **`[longitude, latitude]`**, NOT `[latitude, longitude]`.

---

### 3.2 Add a Pin with a Popup Label

```javascript
const popup = new maplibregl.Popup({ offset: 25 })
  .setText('تهران — پایتخت ایران');

const marker = new maplibregl.Marker({ color: '#3182ce' })
  .setLngLat([51.3890, 35.6892])
  .setPopup(popup)
  .addTo(map);
```

---

### 3.3 Move the Map Camera (Fly / Jump / Fit)

```javascript
// 1. Smoothly fly to coordinates
map.flyTo({
  center: [51.3890, 35.6892],
  zoom: 14,
  speed: 1.5
});

// 2. Jump instantly without animation
map.jumpTo({
  center: [51.3890, 35.6892],
  zoom: 14
});

// 3. Fit bounds to a province or whole of Iran
map.fitBounds(
  [[44.03, 25.06], [63.33, 39.78]],
  { padding: 40 }
);
```

---

## 4. Location Search & Geocoding API

Search for cities, streets, neighborhoods, and points of interest across Iran with **compound search** (e.g. `فسا خیابان فیاض بخش`) and **proximity re-ranking** based on viewport center.

### 4.1 Endpoint

```
GET /search
```

### 4.2 Query Parameters

| Parameter | Type | Required | Default | Description |
| :--- | :--- | :--- | :--- | :--- |
| `q` | string | **Yes** | — | Search query in Persian or English (e.g. `تهران`, `فسا خیابان فیاض بخش`, `مصلی`) |
| `lat` | float | No | — | Current map center latitude. Results near this coordinate receive high proximity priority. |
| `lon` | float | No | — | Current map center longitude. Results near this coordinate receive high proximity priority. |
| `limit` | integer | No | `7` | Maximum number of results (1–30) |
| `lang` | string | No | `fa` | Response language: `fa` (Persian) or `en` (English) |

### 4.3 Example Requests

```
# 1. Direct city search
GET https://map.netzo.ir/search?q=تهران

# 2. Compound search: city + street name in any order
GET https://map.netzo.ir/search?q=فسا+خیابان+فیاض+بخش

# 3. Proximity search: searches 'مصلی' prioritizing results near Fasa coordinates
GET https://map.netzo.ir/search?q=مصلی&lat=28.9383&lon=53.6482&limit=5
```

### 4.4 Response Format (GeoJSON)

```json
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [53.6482, 28.9383]
      },
      "properties": {
        "name": "خیابان فیاض بخش",
        "display_name": "خیابان فیاض بخش، فسا، استان فارس",
        "type": "street",
        "type_fa": "خیابان",
        "city": "فسا",
        "province": "استان فارس",
        "country": "ایران",
        "distance_km": 1.2,
        "extent": [53.642, 28.934, 53.654, 28.942]
      }
    }
  ]
}
```

## 5. Reverse Geocoding API (Coordinates to Address)

Converts geographic coordinates (latitude, longitude) into a structured Persian address object.

### 5.1 Endpoint

```
GET /api/reverse-geocode
GET /reverse
```

### 5.2 Query Parameters

| Parameter | Type | Required | Default | Description |
| :--- | :--- | :--- | :--- | :--- |
| `lat` / `latitude` | float | **Yes** | — | Latitude coordinate (-90 to 90) |
| `lng` / `lon` / `longitude` | float | **Yes** | — | Longitude coordinate (-180 to 180) |

### 5.3 Example Requests

```
# 1. Reverse geocode a point in Tehran (Azadi Tower)
GET https://map.netzo.ir/api/reverse-geocode?lat=35.6997&lng=51.3380

# 2. Reverse geocode a street in Fasa
GET https://map.netzo.ir/api/reverse-geocode?lat=28.9383&lng=53.6482
```

### 5.4 Standard Response Format

```json
{
  "formatted_address": "میدان آزادی، تهران، استان تهران",
  "street_number": null,
  "street": null,
  "neighborhood": "منطقه ۹",
  "district": "تهران",
  "city": "تهران",
  "state": "استان تهران",
  "postal_code": null,
  "country": "ایران",
  "country_code": "IR",
  "name": "میدان آزادی",
  "type": "place",
  "type_fa": "مکان",
  "distance_km": 0.05,
  "latitude": 35.6997,
  "longitude": 51.338
}
```

### 5.5 Client Helper Function (Vanilla JS)

```javascript
/**
 * Reverse geocode latitude & longitude to structured address JSON.
 * @param {number} latitude
 * @param {number} longitude
 * @returns {Promise<Object>} Normalized address object
 */
export async function reverseGeocode(latitude, longitude) {
  if (typeof latitude !== 'number' || typeof longitude !== 'number') {
    throw new Error('INVALID_COORDINATES: Latitude and longitude must be numbers.');
  }

  const url = `https://map.netzo.ir/api/reverse-geocode?lat=${latitude}&lng=${longitude}`;
  const res = await fetch(url);

  if (!res.ok) {
    const errorData = await res.json().catch(() => ({}));
    throw new Error(errorData.message || `Reverse geocoding failed: HTTP ${res.status}`);
  }

  return await res.json();
}
```

---

## 6. Complete Combined Example (Search + Click + Reverse + Pin)

A complete, production-ready Persian interactive web page featuring search, map click-to-reverse-geocode, and marker controls:

```html
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
  <meta charset="UTF-8" />
  <title>جستجو و آدرس‌یابی روی نقشه نتزو</title>
  <link href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet" />
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    html, body, #map { width: 100%; height: 100vh; font-family: Tahoma, sans-serif; }
    #search-container {
      position: absolute; top: 16px; right: 16px; z-index: 10;
      display: flex; gap: 8px; background: white; padding: 6px;
      border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
    }
    #search-input {
      padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px;
      font-size: 14px; width: 260px; outline: none; direction: rtl;
    }
    #search-btn {
      padding: 8px 16px; background: #0066cc; color: white; border: none;
      border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: bold;
    }
    #search-btn:hover { background: #0052a3; }
  </style>
</head>
<body>
<div id="search-container">
  <input id="search-input" type="text" placeholder="جستجوی مکان یا کلیک روی نقشه..." />
  <button id="search-btn">جستجو</button>
</div>
<div id="map"></div>

<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
<script>
  const SERVER_URL = 'https://map.netzo.ir';

  // 1. Configure RTL Plugin
  maplibregl.setRTLTextPlugin(
    'https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.2.3/mapbox-gl-rtl-text.min.js',
    null,
    true
  );

  // 2. Initialize Map
  const map = new maplibregl.Map({
    container: 'map',
    style: `${SERVER_URL}/styles/iran-persian.json`,
    center: [53.6880, 32.4279],
    zoom: 5.5
  });

  map.addControl(new maplibregl.NavigationControl(), 'top-left');

  let activeMarker = null;

  function placePin(lngLat, label) {
    if (activeMarker) activeMarker.remove();
    activeMarker = new maplibregl.Marker({ color: '#e53e3e' })
      .setLngLat(lngLat)
      .setPopup(new maplibregl.Popup({ offset: 25 }).setText(label))
      .addTo(map);
    activeMarker.togglePopup();
  }

  // 3. Click map to reverse geocode coordinates to Persian address
  map.on('click', async (e) => {
    const { lng, lat } = e.lngLat;
    placePin([lng, lat], 'در حال دریافت آدرس...');

    try {
      const res = await fetch(`${SERVER_URL}/api/reverse-geocode?lat=${lat}&lng=${lng}`);
      const data = await res.json();
      placePin([lng, lat], data.formatted_address || `${lat.toFixed(4)}, ${lng.toFixed(4)}`);
    } catch (err) {
      placePin([lng, lat], `${lat.toFixed(4)}, ${lng.toFixed(4)}`);
    }
  });

  // 4. Search handler
  async function performSearch(query) {
    if (!query.trim()) return;
    try {
      const center = map.getCenter();
      const res = await fetch(`${SERVER_URL}/search?q=${encodeURIComponent(query)}&lat=${center.lat}&lon=${center.lng}&limit=1`);
      const data = await res.json();

      if (!data.features || data.features.length === 0) {
        alert('مکان مورد نظر یافت نشد.');
        return;
      }

      const place = data.features[0];
      const [lng, lat] = place.geometry.coordinates;
      const placeName = place.properties.display_name || place.properties.name;

      placePin([lng, lat], placeName);

      if (place.properties.extent) {
        const [w, s, e, n] = place.properties.extent;
        map.fitBounds([[w, s], [e, n]], { padding: 60 });
      } else {
        map.flyTo({ center: [lng, lat], zoom: 14, speed: 1.4 });
      }
    } catch (err) {
      console.error('Search error:', err);
      alert('خطا در جستجو.');
    }
  }

  document.getElementById('search-btn').addEventListener('click', () => {
    performSearch(document.getElementById('search-input').value);
  });

  document.getElementById('search-input').addEventListener('keydown', (e) => {
    if (e.key === 'Enter') performSearch(e.target.value);
  });
</script>
</body>
</html>
```

---

## 7. React / Next.js / Vue Integration

### 7.1 React Component (`NetzoMap.jsx`)

```jsx
import React, { useEffect, useRef } from 'react';
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';

const MAP_SERVER = 'https://map.netzo.ir';

export function NetzoMap({
  center = [53.6880, 32.4279],
  zoom = 5.5,
  onMapReady,
  style = { width: '100%', height: '500px' }
}) {
  const mapContainer = useRef(null);
  const mapInstance = useRef(null);

  useEffect(() => {
    if (!maplibregl.getRTLTextPluginStatus ||
        maplibregl.getRTLTextPluginStatus() === 'unavailable') {
      maplibregl.setRTLTextPlugin(
        'https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.2.3/mapbox-gl-rtl-text.min.js',
        null,
        true
      );
    }

    mapInstance.current = new maplibregl.Map({
      container: mapContainer.current,
      style: `${MAP_SERVER}/styles/iran-persian.json`,
      center,
      zoom,
      maxZoom: 18,
      minZoom: 4
    });

    mapInstance.current.on('load', () => {
      if (onMapReady) onMapReady(mapInstance.current);
    });

    return () => {
      if (mapInstance.current) {
        mapInstance.current.remove();
      }
    };
  }, []);

  return <div ref={mapContainer} style={style} />;
}
```

---

## 8. Error & Troubleshooting Reference

| Symptom | Cause | Solution |
| :--- | :--- | :--- |
| **Disconnected / reversed Persian letters** | RTL text plugin missing or called after map init | Call `maplibregl.setRTLTextPlugin(...)` **before** creating `new maplibregl.Map()` |
| **Grey map background** | Incorrect style URL or network block | Verify `https://map.netzo.ir/styles/iran-persian.json` is reachable in browser |
| **Pins in wrong location** | Latitude and longitude swapped | Ensure coordinates are passed as `[longitude, latitude]` |
| **Search returns 0 results** | Query typo or remote area | Try broad Persian city name (e.g. `اصفهان`) or omit specific street suffixes |

---

## 9. Coordinate System Reference

All coordinates use **WGS84 (EPSG:4326)** in `[longitude, latitude]` format.

| Major City | Longitude | Latitude |
| :--- | :--- | :--- |
| **Tehran (تهران)** | 51.3890 | 35.6892 |
| **Isfahan (اصفهان)** | 51.6674 | 32.6546 |
| **Mashhad (مشهد)** | 59.6042 | 36.3132 |
| **Shiraz (شیراز)** | 52.5311 | 29.5918 |
| **Tabriz (تبریز)** | 46.2919 | 38.0800 |
| **Iran Geographic Center** | 53.6880 | 32.4279 |

**Iran Full Bounding Box:** `[[44.0327, 25.0644], [63.3333, 39.7820]]`

---

*Netzo Map — Self-Hosted Persian Vector Map & Geocoding Service*  
*Production Server: https://map.netzo.ir/*
