# Buses: live positions, stops, lines and timetables

MapMap carries England's local buses as display data: where the buses are
now, where the stops are, which lines call at them, and what the timetable
says leaves next. Four pieces, one rule.

| You want | Use | Data |
|---|---|---|
| Buses moving on a map | [`GET /v1/transit/vehicles`](#live-bus-positions) or the SDK's `TransitVehiclesLayer` | DfT Bus Open Data Service (BODS) live positions |
| Stops and lines drawn on a map | [The SDK's `TransitStopsLayer`](#stops-and-lines-on-a-map) | NaPTAN stops, BODS timetable shapes |
| The stops near a point | [`GET /v1/transit/stops`](#stops-near-a-point) or the MCP tool `nearby_stops` | NaPTAN, with lines from BODS timetables |
| What leaves a stop next | [`GET /v1/transit/departures`](#departures-at-a-stop) or the MCP tool `next_departures` | BODS timetables, paired with live positions |

**The rule: display, never prediction.** Positions are drawn where the feed
reports them. Departure times are the timetable's. A departure can show a
bus seen on its line heading towards the stop, which is an observation of
where that bus is, not a forecast of when it arrives. MapMap does not
compute journey times, plan trips on public transport, or predict arrivals
or delays, and nothing on this page should be presented as doing so.

**Coverage.** England's local bus and coach services: BODS is England's
national bus data service. Stops just across the border are included where
an England service calls at them. Scotland and Wales' own networks, rail,
tram, metro and ferry are not included.

## Live bus positions

`GET /v1/transit/vehicles?bbox=west,south,east,north` answers GeoJSON for a
box of at most 4 square degrees, refreshed from the national feed every
minute, with the feed's freshness on every answer (`feed.status` `ok`,
`stale` past 180 seconds, or `unknown`; a stale feed still returns its
buses, marked paused). Full parameters and fields:
[API reference](/docs/api-reference#transit).

```ts
import { MapMapMap, TransitVehiclesLayer } from "@mapmap/maps";

const map = new MapMapMap({ container: "map", apiKey: "snk_..." });
const buses = new TransitVehiclesLayer(map, {
  onFeedStatus: ({ label }) => (pill.textContent = label), // "Live, 42 s ago"
});
buses.start();
```

The layer polls only the area on screen, every 30 seconds by default, and
draws one poll behind so buses glide between fixes.

## Stops and lines on a map

Two map tile archives, rebuilt every night and kept apart from the base
map: `transit-stops` (layer `stops`, zoom 10 to 14) and `transit-lines`
(layer `lines`, zoom 6 to 14). With an API key on the map, the layer reads
them through the keyed tile API
(`https://api.mapmap.ai/tiles/transit-stops/tiles.json`, metered like the
base map tiles), which works from your own site. Self-hosting, or reading
the archives directly, their current addresses are in the tiles index,
`https://tiles.mapmap.ai/index.json`, under `transit`.

```ts
import { MapMapMap, TransitStopsLayer } from "@mapmap/maps";

const map = new MapMapMap({ container: "map", apiKey: "snk_..." });
const transit = new TransitStopsLayer(map, {
  onStopClick: async (stop) => {
    // stop.id is the NaPTAN ATCO code; stop.lines the lines calling there
    const res = await fetch(
      `https://api.mapmap.ai/v1/transit/departures?stop=${stop.id}`,
      { headers: { Authorization: "Bearer snk_..." } },
    );
    showBoard(await res.json());
  },
});
await transit.ready; // false when no transit archives are published
```

- Lines show from zoom 6, in the route's own colour where the timetable
  publishes one. A route the timetable gives no shape for is drawn dashed,
  stop to stop, so it never reads as the road the bus takes.
- Stops show from zoom 11 with the MapMap transit glyph.
- Both layers survive `map.setStyle(...)` and sit in the `middle` slot
  (above roads, below labels) unless you pass `slot`.
- Options: `stops`, `lines` (both default `true`), `slot`, `onStopClick`,
  `lineColour`, `stopColour`, `source` (`"auto"`, `"api"` or
  `"tiles-host"`), and `stopsUrl`/`linesUrl` for a copy you host yourself.

What `@mapmap/maps` exports for this, besides the layer itself:

| Export | What it is |
|---|---|
| `TransitStopsLayerOptions` | The options above |
| `TransitStopFeature` | What `onStopClick` hands you: `id` (ATCO code), `name`, `indicator`, `locality`, `bearing`, `lines` |
| `transitStopFromProperties(props)` | Turns a stop feature's tile properties into a `TransitStopFeature` (parses `lines`) |
| `transitTileJsonUrl(baseUrl, id, apiKey?)` | The keyed TileJSON URL for `transit-stops` or `transit-lines` on a gateway |
| `transitUrlsFromIndex(index, indexUrl)` | The `pmtiles://` URLs a tiles index announces under `transit` |
| `transitArchiveUrl(url, indexUrl)` | One archive path resolved to a `pmtiles://` URL |
| `TRANSIT_TILES_INDEX_URL` | `https://tiles.mapmap.ai/index.json` |
| `NAPTAN_SHORT_CREDIT` | "Bus stops: NaPTAN, DfT, OGL v3.0", the stops credit the layer adds |
| `TRANSIT_LINE_FALLBACK_COLOUR` | The line colour where the timetable publishes none |

Stop properties in the tiles: `id` (ATCO code), `name`, `indicator`,
`locality`, `bearing` (NaPTAN compass point), `lines` (a JSON array as a
string). Line properties: `line`, `operator`, `noc` (National Operator
Code), `colour` (when published), `direction` (`outbound`, `inbound` or
absent), `route`, and `shape` (`gtfs`, or `stops` for a stop-to-stop chain).

## Stops near a point

```sh
curl -s "https://api.mapmap.ai/v1/transit/stops?lat=51.5080&lon=-0.1281&radius_m=300" \
  -H "Authorization: Bearer $KEY"
```

`radius_m` 1 to 2,000 (default 500), `limit` 1 to 50 (default 10). Each
stop carries its ATCO code, name, indicator, locality, position, distance
and bearing from your point, and `lines`. Every answer carries `coverage`
and `attribution`.

## Departures at a stop

```sh
curl -s "https://api.mapmap.ai/v1/transit/departures?stop=490013767T&limit=6" \
  -H "Authorization: Bearer $KEY"
```

```json
{
  "stop": { "id": "490013767T", "name": "Trafalgar Square", "indicator": "Stop T", "lines": ["12", "88", "139", "159", "453"] },
  "basis": "timetable",
  "label": "Timetable times from Bus Open Data timetables. A live entry shows a vehicle on this line observed upstream of the stop. MapMap does not predict arrival times.",
  "departures": [
    { "line": "88", "destination": "Parliament Hill Fields", "operator": "GAHL",
      "scheduled_at": "2026-10-12T18:19:00+01:00", "trip": "VJ...",
      "live": { "vehicle": "YX13BKG", "distance_m": 62, "age_s": 154 } }
  ],
  "timetable_date": "2026-10-12",
  "coverage": "England bus stops (NaPTAN) with services from BODS timetables. Rail, tram and ferry are not included.",
  "attribution": "Contains public sector information licensed under the Open Government Licence v3.0. Source: NaPTAN, Department for Transport. ..."
}
```

- `at` (RFC 3339, default now), `window_minutes` (1 to 180, default 60),
  `limit` (1 to 30, default 8). The index holds three service days from
  today; an `at` outside them is refused with the window named.
- **Show the `label` with the departures.** It says what the times are and
  what a live entry is.
- `live` appears when a bus on the same line, seen in the last five
  minutes, is within 3 km of the stop and its reported heading points
  towards it. Per line, the nearest such bus goes with the earliest
  departure. It is a heuristic over live positions, not a link between that
  bus and that timetabled journey.

## For agents

The MCP tools `nearby_stops` and `next_departures` return the same answers,
with `label` and `coverage` on every one. Pass them on: an agent must not
present a timetable time or a live entry as an arrival time. See
[MCP](/docs/mcp).

## Keys and metering

`/v1/transit/vehicles`, `/v1/transit/stops` and `/v1/transit/departures`
are one Standard unit per request each, and a browser (`maps`) key may call
them. The stops and lines tiles meter like the base map tiles when read
through the keyed tile API. A live map polling every 30 seconds makes 2,880 calls a day
per open page, so poll the visible area only and prefer a longer interval
where the use allows.

## Licence and credits

All of this is public sector information licensed under the
[Open Government Licence v3.0](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/):

- Stops: "Contains public sector information licensed under the Open
  Government Licence v3.0. Source: NaPTAN, Department for Transport."
- Live positions and timetables: "Contains public sector information from
  the Bus Open Data Service, licensed under the Open Government Licence
  v3.0. Source: Department for Transport (data.bus-data.dft.gov.uk)."

Every API answer carries its `attribution`; the SDK layers add the short
credits ("Bus stops: NaPTAN, DfT, OGL v3.0" and "Bus data: DfT Bus Open
Data Service, OGL v3.0") to the map's one credit line. The transit data is
served from its own files and paths and is never merged into the
OpenStreetMap-derived base map.
