A coordinate is not a service update
The first collector implementation stored bus observations. That was enough to put a marker on a map, but it was not enough to tell a rider whether the marker was current, whether the bus was actually moving, or when it would reach a stop. A vehicle report has a position and a timestamp. The useful product claims come later, after the system checks the report against a route and against other reports.
That distinction is why the tracker, arrival estimates, and traffic messages do not share one latestLocation value. The tracker shows the latest collector snapshot and its age. Transit intelligence derives trips, stop passages, dwell visits, and segment travel times. Road intelligence uses a stricter moving-vehicle branch before it says anything about traffic.
What the tracker publishes
GET /api/vehicles returns the collector's public vehicle snapshot. Each item carries the vehicle ID, fleet ID, route ID, directional variant, position, speed, bearing, status, and the upstream reportedAt time. The web tracker turns those fields into a compact list such as a route, fleet number, movement text, and report age. Selecting a vehicle loads its historical profile separately; the list itself is not an ETA calculation.
The API allows a fifteen-second public cache with stale-while-revalidate for thirty seconds. The collector marks its stored snapshot as stale once its generated time is more than three collection intervals old, which is sixty seconds at the current twenty-second cadence. A stale snapshot remains evidence that the collector saw something earlier. It is not presented as a fresh position.
The public state has a small lifecycle. It answers one question: can the tracker show a snapshot without hiding how old it is?
The Delayed state does not mean that every vehicle is wrong. It means the snapshot as a whole has crossed the collector's public age threshold. Unavailable is different again: the API has no snapshot to return and responds with an unavailable error. Data health uses its own internal categories, so the tracker state should not be confused with the collector dashboard's fresh, stale, and expired counts.
The packet the system starts with
The upstream-normalization test combines an active-vehicle fixture with a location response. A normalized result looks like this. It shows the two clocks that matter: when the vehicle says the report was made and when the collector says it received the report.
{ "vehicleId": "DCBUS-01", "fleetId": "DCBUS 01", "routeId": "R102", "direction": "PM", "lat": 7.040452, "lng": 125.535606, "speedKph": 44.03, "bearingDeg": 164, "status": "online", "reportedAt": "2026-08-17T09:16:00.000Z", "collectedAt": "2026-08-17T09:16:05.000Z"}The collector stores both timestamps in vehicle_observations. reportedAt is used to order movement and measure age. collectedAt records ingestion latency. Treating collectedAt as the vehicle's position time would make a delayed packet look newer than it is.
The failure path is deliberately partial. The collector test makes the location request for TRACK-02 return HTTP 503 while another active vehicle succeeds. The collection result still reports two active vehicles, one valid observation, one stored observation, and one failure. The successful row is not thrown away because a different vehicle failed. If the D1 batch itself fails, the collector does not run derived intelligence from that batch; raw persistence is the boundary.
From a report to a useful bus event
The collector first projects a report onto the canonical route for its route ID and direction. The current projector accepts a point as good when its distance from the route geometry is at most 200 meters. A point farther away is stored as off_route. The position record also stores route progress, the current stop segment, and the nearest stop. There is no current heading-delta rule in this path, so the older documentation's 120-meter and 60-degree limits no longer describe the code.
A good point can still be unusable for movement history. If the previous good position for the same vehicle, route, and direction is within ten minutes and the progress change implies more than 100 km/h, the new position is stored as gps_jump. It does not advance a trip, create stop passages, or create a dwell visit. The system keeps the suspect position so the dashboard can show that the report was received and rejected for derivation.
Trip continuity then gives a sequence of good positions an identity. A vehicle starts an open trip_run when no open run exists. A route or direction change, a gap longer than ten minutes, or a route wrap closes the old run and starts another. A run is marked completed only when its progress began in the first quarter of the route and reached at least three quarters. Other closed runs are marked abandoned; that label describes incomplete evidence, not a claim about the bus operator.
The same progress stream feeds two transit observations. Crossing an ordered stop creates a stop_passage with an interpolated time. A stop-to-stop pair of passages creates a transit_segment_run, which is the material later used for ETA baselines. Separately, a point within 75 meters of its nearest stop at 5 km/h or less starts a stop-visit candidate. The candidate needs two samples before it becomes a stop_visit, and the stored uncertainty is 40 seconds, or two twenty-second collection intervals.
What an arrival estimate is allowed to say
The arrival service has stricter rules than the marker list. Stop-arrival queries consider only the latest good position for each vehicle when it is no more than 120 seconds old and still before the requested stop. The ETA calculator then marks confidence low once the position age exceeds 90 seconds. Those thresholds serve different decisions: 120 seconds controls which vehicles enter the stop-arrival query, while 90 seconds controls how much trust the estimate receives.
Historical travel evidence needs 30 segment runs before it becomes a baseline. If a segment has fewer runs, the calculator falls back to a fixed 18 km/h bus speed and reports that the estimate contains fallback segments. When a historical baseline exists, live adjustment considers the last 30 minutes and groups samples by vehicle. At least three independent vehicles must support a slowdown of 1.3 times the baseline before the ETA is adjusted. Five vehicles preserve high confidence; three or four lower the adjusted estimate to moderate confidence.
That is why a marker should not carry a precise arrival time just because it has a recent coordinate. The position can be good while the segment has no historical evidence, the position can be too old, or the live sample can come from only one bus. In each case the API exposes evidence or an unavailable state instead of manufacturing precision.
A stopped bus is not automatically traffic
The stop detector and the road detector answer different questions. A bus moving slowly inside a stop area may be boarding passengers. Road intelligence excludes observations within 100 meters of a stop and accepts only moving intervals whose route-derived speed is between 7 and 100 km/h. It then asks Valhalla to match a bounded trace to road edges and requires corroboration across vehicles before returning a slowdown.
The transit versus road intelligence experiment follows that split in detail. The short version is that a stopped bus can be valuable evidence for dwell time and invalid evidence for road congestion at the same moment.
Why the feature grew this way
The history is visible in the implementation sequence. The August 18 collector commit added raw bus observations. The next day's transit-event change added route positions, GPS-jump handling, trips, passages, and stop visits. Road matching and bounded analytics arrived afterward, followed by live ETA adjustment. The product stayed a simple snapshot list while the evidence behind it became more careful.
The remaining limit is data coverage. A clean packet can still be off the canonical route, a route can have too few segment runs, and an optional road matcher can be unavailable. The tracker can tell a rider what was last observed. It cannot turn missing observations into certainty.
Current sources: collector.ts, network.ts, intelligence.ts, analytics.ts, and the collector tests. The concrete packet comes from upstream.test.ts. The feature history begins with 01b7781 and 521b6db.