Background
Captured 2026-07-06 during field testing alongside #206. Two snapshots from the rider's device showed the prediction pipeline producing 7–8 minute ETAs for vehicles that physically have one or zero stops to go — i.e. a bus that should read "1–2 minutes".
Symptoms
IMG_1056.PNG — vehicle ~1 stop away, live GPS, ETA reads "arriving in 8 min". Expected: 1–2 min given the speed config.
IMG_1058.PNG — vehicle past the last station (geographically shown south of the user's stop) still appears in the drop-off section as "in 7 min". Expected: bucketed as departed (negative signed distance along the trip shape).
The drop-off vs arriving/incoming distinction means a fast rider would never board that vehicle anyway, but the displayed "arrives in N min" is the user-facing surface of the same bug: the ETA computation thinks the bus is somewhere it isn't.
Likely cause
predictArrivalAlongShape.ts uses signedDistM = stopProj.distAlongM - vehProj.distAlongM along the trip's polyline. When the polyline contains a terminal loop or a small post-terminus turnaround, a vehicle physically past the stop can project onto a polyline segment that lies before the stop in distAlongM terms — so the function reads a phantom future distance and times it against the time-of-day fallback (default kmh_offpeak: 25, "outskirts" tier 4 even lower).
The same loop distorts ETA in #1: the bus is close in haversine distance but the cumdistM walk winds through the loop and produces several minutes worth of time at the conservative TOD-offpeak speed.
Repro ingredients
- An active trip shape whose tail end includes a small loop (the GTFS-RT
vehicle.position lives south of the user's stop on the loop-back segment).
- Per-feed
_neary_config.timing.city_centre either absent or the stop outside the centre radius, so tier 3 / tier 4 wins instead of tier 1 (vehicle's own reported speedMs).
Proposed fix (not yet implemented)
Two layers, both pure-function:
(a) Distance sanity clamp. When signedDistM > haversineMeters(veh, stop) × SAFETY_MARGIN (SAFETY_MARGIN ~3), the polyline projection has wandered; fall back to the haversine distance with estimateSegmentSpeed directly.
(b) Tier-2 wiring. Pass nearbyVehicles (a snapshot of the current fleet with matching direction) into the speed cascade so a bus moving at 40 km/h within 1 km promotes tier 2 → tier 1 wins. Bigger lift — touches the worker pipeline.
I'd lean toward shipping (a) with a unit test that drives the "vehicle projects past due to wrap" scenario, then filing (b) separately once the cascade plumbing is ready.
Acceptance
Out of scope here: same idea generalises to map view (/map/route/[id]), which uses the same watchPosition but has a different render cadence. File a follow-up if the stall recurs on the map.
Background
Captured 2026-07-06 during field testing alongside #206. Two snapshots from the rider's device showed the prediction pipeline producing 7–8 minute ETAs for vehicles that physically have one or zero stops to go — i.e. a bus that should read "1–2 minutes".
Symptoms
IMG_1056.PNG— vehicle ~1 stop away, live GPS, ETA reads "arriving in 8 min". Expected: 1–2 min given the speed config.IMG_1058.PNG— vehicle past the last station (geographically shown south of the user's stop) still appears in the drop-off section as "in 7 min". Expected: bucketed asdeparted(negative signed distance along the trip shape).The drop-off vs
arriving/incomingdistinction means a fast rider would never board that vehicle anyway, but the displayed "arrives in N min" is the user-facing surface of the same bug: the ETA computation thinks the bus is somewhere it isn't.Likely cause
predictArrivalAlongShape.tsusessignedDistM = stopProj.distAlongM - vehProj.distAlongMalong the trip's polyline. When the polyline contains a terminal loop or a small post-terminus turnaround, a vehicle physically past the stop can project onto a polyline segment that lies before the stop indistAlongMterms — so the function reads a phantom future distance and times it against the time-of-day fallback (defaultkmh_offpeak: 25, "outskirts" tier 4 even lower).The same loop distorts ETA in #1: the bus is close in haversine distance but the cumdistM walk winds through the loop and produces several minutes worth of time at the conservative TOD-offpeak speed.
Repro ingredients
vehicle.positionlives south of the user's stop on the loop-back segment)._neary_config.timing.city_centreeither absent or the stop outside the centre radius, so tier 3 / tier 4 wins instead of tier 1 (vehicle's own reportedspeedMs).Proposed fix (not yet implemented)
Two layers, both pure-function:
(a) Distance sanity clamp. When
signedDistM > haversineMeters(veh, stop) × SAFETY_MARGIN(SAFETY_MARGIN ~3), the polyline projection has wandered; fall back to the haversine distance withestimateSegmentSpeeddirectly.(b) Tier-2 wiring. Pass
nearbyVehicles(a snapshot of the current fleet with matching direction) into the speed cascade so a bus moving at 40 km/h within 1 km promotes tier 2 → tier 1 wins. Bigger lift — touches the worker pipeline.I'd lean toward shipping (a) with a unit test that drives the "vehicle projects past due to wrap" scenario, then filing (b) separately once the cascade plumbing is ready.
Acceptance
Out of scope here: same idea generalises to map view (
/map/route/[id]), which uses the samewatchPositionbut has a different render cadence. File a follow-up if the stall recurs on the map.