A Single Fix Is a Rumour
Drive between tall buildings, under an overpass or out of a tunnel and your phone's position will occasionally jump. Satellite signals bounce off glass and concrete before they reach the antenna, the receiver solves for a position using a path that is longer than the real one, and the dot lands a street or two away. The nasty part is that the fix does not always look bad. Pathfinder's own test for this case uses a fix 200 m off the road that claims 10 m accuracy.
If one such fix were enough to call the car off route, the app would say "길을 벗어났어. 다시 찾아볼게", ask the provider for a new route from a position the car never occupied, and possibly start the new route with a U-turn. In the middle of a perfectly correct drive. So GuidanceEngine treats a single miss as a rumour and asks for corroboration.
Three Gates
1. Is the fix usable at all? A fix whose accuracy is worse than 120 m (maxMatchAccuracy), or whose coordinate is invalid, is not matched. Notice exactly what that means: it does not count as a miss and it does not reset the miss streak. It is simply not evidence about the route. (Its speed, if any, still feeds the smoothed speed, because that update runs before the gate.)
2. Is it on the route? The fix is projected onto the route line, inside a search window around the last progress, and its lateral distance is compared with a threshold of max(45, accuracy + 20) metres. A fix that admits 60 m of uncertainty gets 80 m of slack; a sharp 5 m fix gets the floor of 45. Before the very first match the threshold is raised to at least 150 m (joinDistance), because a route begins on the nearest road and the car often begins in a car park beside it: joining is judged more loosely than leaving. One more way to miss: on the right road but heading at least 120° away from the route's direction at 4 m/s or more is off route too, because that is a U-turn the route did not ask for.
3. Is the miss confirmed? The engine counts consecutive misses and remembers when the streak began. It declares off-route only when both hold: at least 3 misses (offRouteConfirmations) and at least 4 seconds since the first of them (offRouteMinDuration). Any on-route fix resets both. The phase becomes offRoute and exactly one offRoute(lateral:) event goes out; further misses say nothing more.
| Constant | Driving | Why |
|---|---|---|
offRouteDistance | 45 m | closer than this is on the route |
accuracyMargin | +20 m | a vague fix gets accuracy + 20 of slack |
maxMatchAccuracy | 120 m | worse fixes are not matched at all |
joinDistance | 150 m | the first match, from a car park |
offRouteConfirmations | 3 | misses in a row |
offRouteMinDuration | 4 s | since the first of them |
oppositeCourseDegrees | 120° | wrong way, trusted from 4 m/s |
Why a Count and a Duration
Either gate alone is easy to fool. A count alone falls for a burst: three bad fixes inside one second from the same reflection. A duration alone falls for two isolated fixes that happen to be far apart in time. Together they ask for a sustained disagreement. At one fix per second the duration is the gate that binds (the fifth miss, four seconds after the first, confirms it). When fixes thin out, say one every five seconds near a tunnel mouth, the count binds instead: two misses five seconds apart are not enough, the third is. The sketch below runs exactly those cases.
What Happens After
The engine only reports. The app's session says the off-route line, logs the event with the lateral distance and asks the same provider for a new route in the drive's own mode, passing the car's course so the new route need not begin with a U-turn (MapKit takes no heading, a known limit). While off route the engine widens its search window, three times the look-behind and twice the look-ahead, so it notices the car rejoining. If it does, backOnRoute cancels the pending reroute. If the provider fails, "길을 못 찾았어" is said once. A failure that waiting cannot fix stops the reroute right there: an exhausted quota, missing credentials, a permission or IP restriction, an unsupported region or mode, or an answer for another provider or mode (mismatchedProvider). Any other failure (the service unavailable, no route, an unreadable response, a dropped connection) is retried every 15 seconds while the engine still says off route. When the new route arrives, the session builds a fresh engine on it and feeds it the latest fix at once.
Walking keeps the same three gates with its own numbers: 35 m, 10 seconds, a 100 m join, and a wrong-way angle of 181° so a pedestrian turning on the pavement is never off route. Track 5 takes those apart.