The planner image exists because the network is static for far more requests than it changes. Rebuilding service objects, geometry indexes, and adjacency lists inside a request repeats the same work for every rider. The build pipeline moves that work to publication time and leaves the browser with one versioned artifact to verify and mount.

The artifact is not a compressed copy of an existing JSON file. The compiler first normalizes service identity and direction, validates the bus stop order and jeepney direction inputs, builds ride and transfer edges, and then writes fixed sections. The image is useful only if those stages are deterministic and the runtime can reject an image from a different contract.

The build path

The compiler turns those inputs into one artifact in a fixed order. Validation happens before publication, and the digest names the resulting bytes.

The build script reads the generated bus and jeepney bundles, the explicit jeepney direction file, and the Davao service-area rings. Bus route geometry must contain an ordered stop sequence whose geometry indexes increase. Every jeepney route must have a known direction. The source hashes are recorded in build metadata so the image carries provenance for the inputs used to make it.

The compiler itself has no file-system side effects. The script calls it twice before publication:

example.ts
1 const input = await plannerInput()
2 const first = compilePlannerImage(input)
3 const second = compilePlannerImage(input)
4 if (
5 first.sha256 !== second.sha256 ||
6 !equalBytes(first.bytes, second.bytes)
7 ) {
8 throw new Error("planner image compiler is nondeterministic")
9 }
10
11 const file = `planner-v1-${first.sha256}.wayplan`
12 const manifest = plannerImageManifestSchema.parse({
13 sha256: first.sha256,
14 file,
15 bytes: first.bytes.byteLength,
16 })

Source: packages/transit/scripts/build-planner-image.ts. The important check is the byte comparison. A stable digest alone would not catch a bug that produced different bytes with the same incorrectly reported metadata.

The script writes temporary image and manifest files, renames them into place, and removes older files matching the planner-image pattern. That is a publication detail, not a runtime cache. The checked-out command is:

text
1pnpm --filter @aidrecabrera/transit build:planner-image

Current image contract

The current format is identified by the eight-byte magic value WAYPLAN followed by a null byte and format version 1. Its header is 96 bytes. The section directory starts at byte 96, each directory entry is 16 bytes, and section offsets are aligned to eight bytes. The content ends with a raw 32-byte SHA-256 digest of the bytes before that digest.

The header records the tick duration, coordinate scale, projection tie distance, section count, and the counts for services, stops, coordinates, nodes, edges, geometry, and service-area rings. Coordinates use an E7 scale of 10,000,000. The runtime rejects a header with a different version, tick duration, coordinate scale, projection tie distance, or required section set.

The required sections are:

  • build metadata;
  • string offsets and string bytes;
  • services and stops;
  • E7 coordinates and nodes;
  • edges;
  • forward and reverse adjacency offsets and edge IDs;
  • geometry offsets and coordinate IDs;
  • service segment offsets and segments;
  • service board offsets and board node IDs;
  • service-area ring offsets and coordinate IDs.

This is a fixed-width section contract, not a claim that every runtime read is zero-copy. The reader can inspect section bytes through DataView, but mounting decodes strings and copies numeric sections into arrays used by the planner. That distinction matters when estimating browser memory.

Transfer edges are compiled from meaning

The edge compiler does not connect every nearby route point. Same-mode transfers require a shared transfer key and different service identities. Bus-to-jeepney edges are considered in nearby latitude buckets, then filtered by coordinate distance. A bus candidate without an official stop ordinal is an input error. After that semantic guard, the distance loop applies the transfer bound:

example.ts
1 const walkMm = coordinateDistanceMm(
2 jeepCoordinate,
3 coordinateForNode(busNode, coordinates)
4 )
5 if (walkMm > TRANSFER_MAX_MM) continue

Accepted edges receive a walking cost plus the target mode's boarding wait. The complete implementation is in packages/transit/src/trip-planner/planner-compile-edges.ts. The 400-metre limit is therefore both a semantic boundary and a cost input. It is not a free connection.

Runtime verification

The browser uses the manifest to find the file, checks its byte length, computes SHA-256 over the image content, and checks the digest embedded at the end of the image. The worker repeats the digest check before it mounts the image.

The sequence has two verification points because the first check proves what the browser downloaded, while the second check proves what the worker is about to interpret. A truncated image fails in the header reader. A mismatched digest fails before IMAGE_READY. A format or section mismatch fails during mount.

The web app's Workbox configuration includes wayplan and JSON files in its precache glob and allows assets up to 32 MiB. The current checked-out manifest reports 26,851,432 bytes for the image, but generated size is data-dependent. The planner image is not currently persisted through a planner-specific Dexie or IndexedDB store. Workbox and the browser's CacheStorage lifecycle own the static asset path.

What this pipeline does not promise

The image moves static graph construction out of the request path. It does not make source data authoritative by itself, and it does not make every geometry point a legal boarding location. The build remains dependent on the generated bus and jeepney bundles, the direction file, and the service-area source.

It also does not eliminate runtime work. The worker mounts arrays, creates query overlay arcs, allocates search labels, and materializes trip instructions. The correct claim is that the format reduces repeated parsing and gives the runtime a versioned contract. The stronger claims of zero parse, zero objects, and zero allocations are not supported by the current reader or search code.

The engine page explains how the mounted sections become a graph. The offline lifecycle experiment explains how the image reaches the worker and what fails when it does not.