Vector tiles are a delivery format, not a second route database. The tile build starts from the canonical bus GeoJSON, keeps the route identity fields, and writes a versioned release that the API can serve at different zoom levels. If the manifest cannot be loaded, the web map can use the validated bundle instead of treating a tile outage as missing transit data.

The failure case is simple to reproduce conceptually: the map requests /api/map/tiles/bus/..., the API cannot read bus-routes/manifest.json from R2, and there is no tile URL to construct. The bundle already contains route geometry, so the application has another source. That fallback is less efficient for map delivery, but it is more useful than a blank map.

Publication path

The release is safe to point at only after the versioned tile objects have been uploaded and checked. The manifest is the final pointer in this sequence.

Publishing the manifest last is the useful part of this sequence. A manifest that points at a partially uploaded release would turn an ordinary deployment race into a map-wide failure. The deploy script uploads versioned objects, verifies that the uploaded decoded bytes match the local decoded bytes, and writes bus-routes/manifest.json only after those checks.

Build and validate

The tile builder requires Tippecanoe 2.79.0. Release builds require an explicit minimum and maximum zoom. The current command does not select a range by default:

example.ts
1const args = [
2 `--output-to-directory=${outputDirectory}`,
3 "--force",
4 `--minimum-zoom=${String(range.minZoom)}`,
5 `--maximum-zoom=${String(range.maxZoom)}`,
6 `--layer=${BUS_ROUTE_SOURCE_LAYER}`,
7 "--preserve-input-order",
8 ...BUS_ROUTE_PUBLIC_PROPERTIES.map((property) => `--include=${property}`),
9 GEOJSON_PATH,
10]

The public tile properties are routeRef, routeId, and direction, and the source layer is bus_routes. The release version is derived from a hash that includes the tile set, schema details, zoom range, Tippecanoe version, and build flags. The manifest then points to /api/map/tiles/bus/{version}/{z}/{x}/{y}.pbf.

Validation decodes the PBF files, checks that every feature is in the expected source layer, checks the public property set, checks LineString or MultiLineString geometry, and compares discovered route references with the canonical source. It also writes tile counts, byte totals, bytes by zoom, the largest tile, and a tile-set SHA-256 to the report.

There is no checked-in .generated/bus-route-tiles release directory in this checkout. That means this page can describe the release contract and scripts, but it cannot truthfully name a current tile version, zoom range, tile count, or byte size. The bus route tile builder and validator are the evidence for those details.

Runtime fallback

The web source planner has three states. It waits while the manifest is unresolved, selects tiles when the manifest is valid, and selects the loaded bundle after a confirmed manifest failure. The bundle path uses the same route identity and rendering-layer contract, but it does not use the manifest's source-layer because it is GeoJSON rather than vector-tile data. bus-route-source.test.ts covers these branches; the implementation excerpt is kept in ADR 0001.

The API has separate failure meanings. An invalid tile coordinate returns 400; a valid request for a missing versioned tile returns 404; a missing or invalid manifest returns 503 with MAP_UNAVAILABLE. Tile responses use the MVT content type, an immutable cache policy, and an ETag. The route tile handler is in bus-route-tiles.ts.

The operational sequence, including the current publication-script mismatch, is in the bus line data runbook. The architectural reason for retaining a bundle fallback is recorded in ADR 0001.