Wi-Fi and Cell-Tower Fallback Accuracy Tiers
When GNSS is unavailable, the platform location provider does not stop answering — it answers from Wi-Fi access points, from cell towers, or from a cached fix, and it returns a position with the same shape as a satellite fix. Nothing in the payload announces that the answer just got two orders of magnitude worse. A pipeline that treats every fix identically will happily test a cell-tower position with 780 m of error against a 120 m depot fence and emit a confident trigger. This page sits under fallback routing for GPS dropouts within Core Architecture & Latency Constraints, and it defines the tiers and the admission rule that keeps a degraded fix from producing an undegraded trigger.
Concept and specification
The provider tier determines the error distribution, and the reported accuracy is a poor proxy for it — receivers report a confidence radius under their own model, and those models are calibrated differently per provider. Measured against surveyed references across a mixed fleet:
| Tier | Typical source | Median error | P95 error | Reported accuracy | Reported/actual |
|---|---|---|---|---|---|
| RTK / dual-frequency | Corrected GNSS | 0.4 m | 1.1 m | 1 m | 0.9× |
| GNSS, open sky | 10+ satellites | 4.2 m | 11 m | 5 m | 1.2× |
| GNSS, urban canyon | 5–8 satellites, multipath | 18 m | 74 m | 12 m | 0.7× |
| Wi-Fi, dense AP | Indoor or city centre | 32 m | 140 m | 45 m | 1.4× |
| Wi-Fi, sparse AP | Suburban or industrial | 190 m | 900 m | 60 m | 0.3× |
| Cell tower | LTE/5G triangulation | 780 m | 3,400 m | 500 m | 0.6× |
| Cached / last known | Provider cache | unbounded | unbounded | previous value | — |
The reported/actual column is the finding that matters. In the urban-canyon and sparse-Wi-Fi tiers the receiver understates its own error by factors of 1.4× and 3.3× respectively — precisely the tiers where a geofence decision is most likely to be wrong. A design that sizes its hysteresis band from reported accuracy alone is therefore under-provisioned exactly when it matters, which is why the band must be sized from measured error per tier and selected by tier at runtime.
The admission rule follows directly. A fix may be tested against a fence only when the fence is large relative to the fix’s uncertainty:
where is the tier’s P95 error and is the fence’s inradius. Below that, the fix cannot resolve the fence and the honest output is “unknown” rather than a coin flip dressed as a trigger.
Step-by-step implementation
1. Capture the provider, not just the coordinates. Both mobile platforms expose it — Android through the provider name and a mock-location flag, iOS indirectly through accuracy and source hints — and where the SDK does not surface it, satellite count and accuracy together classify most fixes reliably.
2. Classify into tiers at ingest and stamp the tier on the event. Everything downstream branches on tier, so classifying once at the edge is cheaper and more consistent than re-deriving it.
from __future__ import annotations
from enum import IntEnum
class Tier(IntEnum):
RTK = 0
GNSS_OPEN = 1
GNSS_URBAN = 2
WIFI_DENSE = 3
WIFI_SPARSE = 4
CELL = 5
CACHED = 6
# P95 error in metres, measured against surveyed references — NOT reported.
TIER_P95_M = {Tier.RTK: 1.1, Tier.GNSS_OPEN: 11.0, Tier.GNSS_URBAN: 74.0,
Tier.WIFI_DENSE: 140.0, Tier.WIFI_SPARSE: 900.0,
Tier.CELL: 3400.0, Tier.CACHED: float("inf")}
def classify(provider: str, sats: int | None, acc_m: float, age_s: float) -> Tier:
if age_s > 30.0:
return Tier.CACHED
p = provider.lower()
if "gps" in p or "gnss" in p:
if acc_m <= 2.0:
return Tier.RTK
return Tier.GNSS_OPEN if (sats or 0) >= 9 and acc_m <= 8.0 else Tier.GNSS_URBAN
if "wifi" in p or "wlan" in p:
return Tier.WIFI_DENSE if acc_m <= 60.0 else Tier.WIFI_SPARSE
if "cell" in p or "network" in p:
return Tier.CELL
return Tier.CACHED
def may_test(tier: Tier, fence_inradius_m: float, alpha: float = 0.5) -> bool:
"""A fix may decide a fence only if the fence is big enough to resolve."""
return TIER_P95_M[tier] <= alpha * fence_inradius_m
3. Filter the candidate set by tier before the exact test. The admission rule is a cheap numeric comparison against a value stored on each fence, so applying it during candidate filtering removes work rather than adding it — a cell-tower fix in a city with 4,000 small fences drops to the handful of large ones it can actually resolve.
4. Emit UNKNOWN rather than nothing. A fence the fix cannot resolve is not “outside”; suppressing the evaluation silently makes a device appear to have left. Emit an explicit unresolved marker so the state machine can hold its committed state rather than infer a departure.
5. Age out cached fixes aggressively. A cached fix carries a fresh timestamp and stale content, which is the worst combination in the whole system — it defeats the clock-drift estimator, the speed gate and the segment join simultaneously. Treat any fix whose provider-reported age exceeds a few tens of seconds as CACHED and refuse to test it against anything.
6. Publish the tier with the trigger. Downstream consumers need to know a trigger came from a 780 m fix. The confidence field defined in graceful degradation strategies for location APIs is where it belongs.
Benchmark and verification
Measured over a week on a fleet with 22% of fixes from non-GNSS providers:
| Policy | False triggers | Missed triggers | Fences evaluated per fix | Unknown emissions |
|---|---|---|---|---|
| Treat all fixes equally | 6.10% | 0.20% | 3.9 | 0.0% |
| Reject non-GNSS entirely | 0.40% | 4.90% | 3.1 | 0.0% |
| Tier + admission rule (α = 0.5) | 0.37% | 0.31% | 2.4 | 3.8% |
| Tier + admission + per-tier band | 0.31% | 0.24% | 2.4 | 3.8% |
Rejecting non-GNSS outright — the fix teams reach for first — trades one error for another: false triggers fall 15× and missed triggers rise 24×, because a 32 m Wi-Fi fix is perfectly capable of deciding a 2 km congestion zone. The admission rule keeps both under 0.4% by asking the only question that matters, which is not “how good is this fix” but “is this fix good enough for this fence”. The candidate column shows the rule pays for itself: evaluating 2.4 fences per fix instead of 3.9 is a 38% reduction in exact-containment work.
The unknown-emission rate is the honest cost, and it should be reported rather than hidden. 3.8% of evaluations produce no decision, concentrated in indoor and rural populations, and the right response is either better fences or better hardware for those populations — not a lower .
Verify by joining triggers against tier and checking that false-trigger rate is roughly flat across tiers. A rate that rises with tier index means is too permissive; a rate near zero for the poor tiers alongside a high unknown rate means it is too strict.
Failure modes and edge cases
| Failure mode | Signature | Mitigation |
|---|---|---|
| Trusting reported accuracy | Urban-canyon and sparse-Wi-Fi fixes under-provisioned | Use measured per-tier P95, not the reported value |
| Cached fix treated as fresh | Speed gate and segment join both corrupted | Classify by provider-reported age; refuse tests on CACHED |
| Unknown emitted as outside | Devices appear to leave fences while indoors | Emit an explicit unresolved marker; hold committed state |
| Fence inradius not stored | Admission rule needs a geometry call per evaluation | Precompute inradius at fence authoring time |
| Tier boundaries tuned on one city | Classification wrong for a different AP density | Re-fit tier thresholds per market |
| Mock locations admitted | Confident triggers from a spoofed provider | Treat the mock flag as its own tier and refuse all tests |
The fourth row is a performance trap rather than a correctness one. Computing a polygon’s inradius is expensive — it is a maximum-inscribed-circle problem — and doing it per evaluation would cost more than the containment test it is guarding. Compute it once when the fence is created, store it beside the geometry, and recompute on edit; a cheap lower bound such as half the minimum bounding-box side is adequate when the exact value is unavailable, and errs toward refusing rather than admitting.
The last row deserves emphasis because it is the one with an adversary. A device reporting mock locations is not degraded, it is lying, and the correct response is not a wider band but a refusal to derive any trigger with commercial consequence from it. The gate belongs at ingest, alongside the speed gate, with its own rejection stream.
Related
- Fallback Routing for GPS Dropouts — the parent topic and the state machine that consumes these tiers.
- Graceful Degradation Strategies for Location APIs — the contract a degraded trigger travels under.
- Choosing Hysteresis Buffer Widths from GPS Error Distributions — sizing the band separately for each tier here.