Measuring Indoor Route Quality in Production

Part of Wayfinding API Observability. A route that is valid, fast to compute and impossible to follow produces no error anywhere. These are the measures that notice it.

Proxies for an Experience Nobody Reports

Six computable proxies for route quality A table of six proxy measures. Detour ratio is the profile route length divided by the default route length and is bad above 2.5, where the accessible route amounts to a refusal. Reroute rate is reroutes per session, bad above 0.6, and indicates the map and positioning disagree. Instruction gap is the maximum metres between turns, bad above 45, and indicates missing junction nodes. First-fix delay is the seconds to a snapped fix, bad above 8. Abandonment is the share of sessions with no arrival, bad above 0.25. Tier share is the percentage of routes served beyond the first fallback tier, bad above 3 percent. Six measures, no user survey required Proxy Computed from Bad above What it means detour_ratio profile route / default route △ 2.5 the accessible route is a refusal reroute_rate reroutes / session △ 0.6 map and positioning disagree instruction_gap max m between turns △ 45 junction nodes are missing first_fix_delay s to a snapped fix △ 8 the session starts unusable abandonment sessions with no arrival △ 0.25 the route was not followed tier_share % beyond tier 1 △ 3 upstream degradation None of these asks a user anything, and all of them move when the experience is bad.

Proxies, not truths. Each one correlates with a bad experience and none of them proves one — which is fine, because a metric that moves reliably when things get worse is what alerting needs.

Route quality has no organic failure signal. A user who is sent the long way round either walks it or gives up, and neither outcome reaches an error log. So the measures are proxies: quantities that can be computed from data the service already has, and that move in the right direction when the experience degrades.

Each one is defined precisely enough to be comparable across releases, which matters more than whether the definition is the ideal one. Detour ratio compares a profile’s route length against the default profile’s for the same origin-destination pair. Reroute rate counts how many times a session requests a new route after the first. Instruction gap is the largest distance between consecutive turn instructions on a route. Abandonment requires the session state machine below.

Detour Ratio Points at the Building

Step-free detour ratio across nine buildings in one estate A ranked series of ninetieth-percentile step-free detour ratios across nine buildings. Seven buildings sit between 1.2 and 2.4, which is a modest detour. Two sit at 3.6 and 4.1, meaning a wheelchair user's route is roughly four times the default route; both are buildings with a single lift at one end of the plate. Seven buildings are fine; two need a second lift 1 2 3 4 1 3 5 7 9 building (ranked) p90 step-free detour ratio two buildings with one lift each

The two outliers are an estates problem, not a software one. No routing change fixes a building with one lift at the far end — but nobody can prioritise a second lift without this number.

Detour ratio is the most valuable of the six because it is the only one that measures accessibility outcomes, and it is the one most often absent. A wheelchair user routed four times as far as an ambulant user has been served successfully by every conventional metric: the request returned 200, the route was valid, the profile was honoured.

Computing it is cheap when a route is requested with a non-default profile — solve twice, once with the requested profile and once with the default, and record the ratio. The second solve costs a few milliseconds on a campus graph and only happens for the minority of requests that use a profile.

What makes it worth the trouble is that the outlier buildings are actionable in a way software metrics rarely are. A p90 of 4.1 in one building is not a routing bug — it is a building with one lift at the wrong end — and it belongs on an estates backlog with a number attached.

Defining Abandonment

The session state machine abandonment is measured from Four states. A session starts when a route is requested. It moves to Following on the first snapped fix that lies on the route. It reaches Arrived when a fix lands within five metres of the destination. From Following it moves to Abandoned if there is no fix for ninety seconds or the user goes persistently off-route. Four states, and the definitions are the hard part first snapped fix destination reached silence, or off-route Started route requested Following fixes on the route Arrived within 5 m of the destination Abandoned no fix for 90 s

Abandonment needs a definition before it needs a threshold. Ninety seconds of silence is a choice — and stating it is what makes the metric comparable between buildings and across releases.

Abandonment needs a session model, and the model is where the judgement sits:

import logging
from dataclasses import dataclass, field

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)


@dataclass
class Session:
    session_id: str
    destination: tuple[float, float]
    level: float
    started_at: float
    fixes: list[tuple[float, float, float, float]] = field(default_factory=list)
    reroutes: int = 0

    ARRIVAL_M = 5.0
    SILENCE_S = 90.0

    def outcome(self, now: float) -> str:
        """Classify a session as arrived, abandoned, or still in progress."""
        if not self.fixes:
            return "no_fix"                       # never got a usable position at all
        t, x, y, level = self.fixes[-1]
        if level == self.level:
            dx, dy = x - self.destination[0], y - self.destination[1]
            if (dx * dx + dy * dy) ** 0.5 <= self.ARRIVAL_M:
                return "arrived"
        return "abandoned" if now - t > self.SILENCE_S else "in_progress"


def summarise(sessions: list[Session], now: float) -> dict[str, float]:
    outcomes = [s.outcome(now) for s in sessions]
    total = max(len(outcomes), 1)
    summary = {o: outcomes.count(o) / total for o in set(outcomes)}
    summary["reroutes_per_session"] = sum(s.reroutes for s in sessions) / total
    logger.info("session summary: %s", {k: round(v, 3) for k, v in summary.items()})
    return summary

Three definitional choices are visible there and all three are arguable. Five metres as arrival is generous enough to survive positioning error and tight enough to distinguish arriving from passing. Ninety seconds of silence as abandonment is long enough to survive a lift ride and a conversation. And no_fix is kept separate from abandoned, because a session that never got a position is a positioning failure rather than a routing one — collapsing them would make a positioning outage look like a route-quality collapse.

Which Metric Points Where

Metric moves Likely cause Owner
detour_ratio up in one building lift coverage estates
detour_ratio up everywhere a profile’s weights changed routing
reroute_rate up after a publish graph moved under users map build
reroute_rate up with no publish positioning degraded positioning
instruction_gap up junction nodes lost in a rebuild graph construction
abandonment up, others flat the map is wrong in a way routing cannot see survey
no_fix up beacons or APs down infrastructure

The last row is the reason no_fix is separated out. A building whose beacons have failed produces sessions that never start, and every other metric on this page is computed only over sessions that did — so a positioning outage makes route quality look better by removing the difficult sessions from the denominator.

Common Errors & Fixes

Detour ratio is 1.0 everywhere. It is being computed against the same profile, not against the default. The comparison must be profile-versus-default for the identical pair, which means solving twice.

Reroute rate spikes on Mondays. Sessions are being counted across app restarts. A session id that survives a restart merges two genuine sessions into one and inflates the reroute count; scope it to the navigation session rather than to the app install.

Abandonment is 60% and nothing is wrong. The silence threshold is too short, or arrival is too tight. Both are worth validating by walking a handful of routes with the metric running — a five-minute exercise that usually moves both numbers.

Instruction gap is huge on long corridors. That may be correct: a 60 m straight corridor genuinely has no turns. The metric is more useful as a change detector than as an absolute, which is why the threshold matters less than the trend against map version.

Integration Point

These measures are derived from the spans and metrics that OpenTelemetry instrumentation emits, and they belong on the route-quality dashboard described in Wayfinding API Observability — the one whose audience is the product owner rather than the on-call engineer.

Two of them feed further. Reroute rate is one of the inputs alerting on positioning degradation uses to separate a positioning regression from a map regression. And detour ratio, broken down by building, is the number that makes a case for a second lift — which is the rare metric whose consumer is not an engineering team at all.

Frequently Asked Questions

Is detour ratio worth the second solve?

Yes, because the second solve only runs for non-default profiles and those are a small minority of traffic. On a campus graph a solve is single-digit milliseconds, so computing the default route alongside a step-free one adds a few milliseconds to perhaps five percent of requests — an overhead nobody will notice. What it buys is the only quantitative view of accessibility outcomes most estates ever get, and the measurement is otherwise impossible after the fact, because the default route for that specific pair at that specific map version is not recoverable later.

How do I tell a bad route from a user who changed their mind?

You cannot, per session, and you do not need to. Individual sessions are noisy — people stop for coffee, take a call, decide to go somewhere else — and no definition of abandonment separates that from a failed route. What the metric is for is comparison: the same definition applied to the same building before and after a publish, or to two buildings in the same week. A rate that jumps from 18% to 34% after a map release is signal regardless of what any individual session meant.

Should route quality metrics be per building or per estate?

Per building, aggregated to the estate for reporting. Buildings differ enormously in layout, lift coverage and signage quality, so an estate-wide average is dominated by whichever buildings have the most traffic and hides the ones with the worst outcomes. The detour-ratio chart above is the clearest case: the estate average is a perfectly respectable 2.0, and two buildings in it route wheelchair users four times as far as everyone else.

This page is a companion to Wayfinding API Observability, part of the Production-Ready Indoor Map Deployment section.