> ## Documentation Index
> Fetch the complete documentation index at: https://developer.plugchoice.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a map

> Draw the map's tiles in any map library, filter them, open a location and search by EVSE id.

The Map API gives you clusters and pins per map tile as JSON, and your map library draws them as its own markers. Any library that tells you its visible area and zoom works: MapLibre, Mapbox, Leaflet, Google Maps, or a native map on iOS and Android.

## Ask for the visible tiles

Tiles are numbered as XYZ map tiles are: at zoom `z` the world is `2^z` by `2^z` tiles, `x` counted from the west and `y` from the north. Zoom runs from 0 to 22.

When the map stops moving, work out the tiles that cover the visible area and ask for each:

```typescript theme={null}
const BASE = "https://roaming.plugchoice.com/map/v1";

function tileOf(lat: number, lng: number, z: number) {
  const n = 2 ** z;
  const rad = (lat * Math.PI) / 180;
  const x = Math.floor(((lng + 180) / 360) * n);
  const y = Math.floor(((1 - Math.log(Math.tan(rad) + 1 / Math.cos(rad)) / Math.PI) / 2) * n);
  return { x: Math.min(Math.max(x, 0), n - 1), y: Math.min(Math.max(y, 0), n - 1) };
}

function visibleTiles(bounds: { north: number; south: number; east: number; west: number }, z: number) {
  const topLeft = tileOf(bounds.north, bounds.west, z);
  const bottomRight = tileOf(bounds.south, bounds.east, z);
  const tiles = [];
  for (let x = topLeft.x; x <= bottomRight.x; x++) {
    for (let y = topLeft.y; y <= bottomRight.y; y++) {
      tiles.push(`${BASE}/tiles/${z}/${x}/${y}`);
    }
  }
  return tiles;
}
```

Use the map's zoom rounded down as `z`. The clusters are sized for a tile drawn 512 px wide. If your library counts zoom for 256 px tiles, as Leaflet and Google Maps do, ask for one zoom lower than the map shows (`z = floor(zoom) - 1`). A map that crosses the antimeridian needs the tiles on both sides.

## Draw clusters and pins

```json theme={null}
{
  "clusters": [[52.09074, 5.12142, 14, 38, 20, 15, 3]],
  "pins": [
    {
      "ids": ["remote-12"],
      "lat": 52.37403,
      "lng": 4.88969,
      "name": "Station",
      "connectors": 2,
      "available": 1,
      "occupied": 1,
      "out_of_order": 0,
      "power_types": ["AC"],
      "max_power_kw": 22.1,
      "price_per_kwh": 0.39
    }
  ]
}
```

Up to zoom 14 a tile is a grid of 8 by 8 cells. Each cell with locations is a cluster, an array of `[lat, lng, locations, connectors, available, occupied, out_of_order]` at the centroid of its locations. Show a cluster as a bubble with its number of connectors, and colour it by the counts if you like. Tapping a cluster usually zooms in on it.

From zoom 15 every location is a pin. Up to zoom 14 a cell whose locations all share one spot is a pin too. Draw every cluster and pin of every tile: a location is in one tile at each zoom, so nothing shows twice.

Locations at the very same coordinates are one pin with all their `ids`. The `name` is the first one's. If a pin has more than one id, let your user page through them in the detail panel.

## Filter

Add filters to the tile's query string. A filtered tile counts only the connectors that match and leaves out a location none of whose connectors do.

| Parameter | Only |
| - | - |
| `available=1` | available connectors |
| `power_type=AC` or `power_type=DC` | AC or DC connectors |
| `connector=IEC_62196_T2,IEC_62196_T2_COMBO` | connectors of any of these OCPI standards, comma-separated or repeated |
| `min_kw=50` | connectors of at least this many kW |
| `max_price=0.45` | connectors whose price per kWh now, before VAT, is at most this many euros |
| `always_open=1` | locations open 24/7 |

```bash theme={null}
curl "https://roaming.plugchoice.com/map/v1/tiles/12/2102/1350?available=1&power_type=DC&min_kw=50"
```

A filter that isn't valid is a `422` that names it, for example `{"error": "power_type is AC or DC"}`.

## Open a location

When your user taps a pin, read the location by one of its `ids`:

```bash theme={null}
curl https://roaming.plugchoice.com/map/v1/locations/remote-12
```

The answer has the address, opening times and facilities, each charger (EVSE) with its live status and connectors, and each connector's tariffs. Once a location has two weeks of history it also has `occupancy`, how busy it usually is per hour of the week, and `out_of_order_ratio_30d`. See [Locations and prices](https://developer.plugchoice.com/map/locations.md) and the [reference](https://developer.plugchoice.com/map/api/get-location.md).

## Search by EVSE id

Search for the EVSE id printed on a charger, or for its start, from 3 characters:

```bash theme={null}
curl "https://roaming.plugchoice.com/map/v1/search?q=NL*GFX*EACE0673532"
```

```json theme={null}
{
  "results": [
    { "location_id": "remote-12", "evse_id": "NL*GFX*EACE0673532*1", "name": "Station", "lat": 52.37403, "lng": 4.88969 }
  ]
}
```

Case, `*` and spaces don't count. Exact matches come first, then ids that start with `q`, at most 10. Move the map to the result and open its `location_id`.

The search finds chargers only. For addresses and places, use your map library's geocoding.

## Ask again without downloading again

Keep each answer with its `ETag`, and send it back in `If-None-Match` when you ask for the same tile again. If nothing changed, the answer is an empty `304` and you draw what you kept. Browsers do this for you: a `fetch` revalidates by itself, because every answer is `Cache-Control: no-cache`.

```typescript theme={null}
const kept = new Map<string, { etag: string; body: unknown }>();

async function getTile(url: string) {
  const known = kept.get(url);
  const response = await fetch(url, { headers: known ? { "If-None-Match": known.etag } : {} });
  if (response.status === 304 && known) return known.body;
  if (response.status === 429) return known?.body ?? null; // try again on the next move
  const body = await response.json();
  kept.set(url, { etag: response.headers.get("ETag") ?? "", body });
  return body;
}
```

Outside a browser, as above, keep the ETags yourself. A map that pans fast asks for a few screens of tiles a second, which stays well within the [rate limit](https://developer.plugchoice.com/map/requests.md#rate-limit).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.