The question behind this experiment was narrow: can a textual jeepney route description become a useful candidate geometry when the repository does not have an authoritative ordered stop list? The tempting answer was to geocode the landmarks and ask a road-routing service to connect them.
That answer produced lines, but a line is not the same thing as evidence that a jeepney serves the line. A geocoder can return a plausible place with the wrong local meaning. A routing service can choose a road that is legal for a car but not used by the service. A route can pass close to another route without creating a transfer. The tool therefore became a candidate generator with a review boundary, not an automatic truth machine.
Candidate-generation notebook
The tool keeps the uncertain steps visible. A candidate can leave the pipeline for human review or remain outside canonical output when its evidence is weak.
The route specifications are version 3 input data. They name places such as Baguio District – Roxas Ave., Toril – Astorga, and Panabo City – Roxas Ave.. The current file contains 27 specifications, but no checked-in output report proves that all of them produced an acceptable candidate.
The tool first resolves each anchor inside either the city bounding box [125.15, 6.85, 125.82, 7.43] or the regional bounding box [124.72, 6.65, 125.85, 7.45]. It keeps up to four choices, applies a 300 metre snap limit, and then selects a sequence that balances geocoder rank against continuity with neighboring anchors. The continuity selector is important because “first result” is not a route-level decision.
A real ambiguity: Mahayag – Bunawan
The correction file changes the semantic path for Mahayag – Bunawan. Its note says the route evidence identifies Mahayag through Cabantian, reaches downtown via Buhangin and Bajada, and returns through Cabantian rather than Bunawan proper. Dacudao Avenue is used as the routable corridor anchor for the Bajada-Dacudao flyover reference.
The correction does not claim that every named place is a stop. It records how the project chose anchors that constrain the route without pretending that a place label by itself defines a service path. The same file contains a Malabog – Roxas Ave. correction that intentionally omits an unresolved TF Davao checkpoint and uses DPWH Depot as the Panacan landmark. Leaving a disputed anchor out is safer than giving it a false coordinate.
These corrections are current tool inputs in route-corrections.json. They are not operator verification, and they should remain visibly labelled as project evidence.
Why long routes use overlapping chunks
Mapbox requests are limited to 25 coordinates in the tool configuration. A route with more anchors is split into overlapping chunks. The shared anchor gives the stitcher a concrete join point, and the stitcher refuses to bridge a gap with an invented straight segment.
chunks = _chunk_indexes(len(coordinates), max_coordinates)chunk_candidates: list[CandidatePath] = []for chunk_index, indexes in enumerate(chunks, start=1): payload = self._request(plan, indexes, False) parsed = self._parse_candidates(plan, payload, request_count=1, indexes=indexes) if not parsed: raise RuntimeError(f"Mapbox returned no route for chunk {chunk_index}/{len(chunks)}") chunk_candidates.append(parsed[0])return (_stitch_chunks(plan, tuple(chunk_candidates)),)The stitcher requires both the geometry gap and the shared-anchor gap to be at most 2 metres. When a router returns NoSegment, the tool may make a diagnostic request with an unlimited radius to explain the failure, but it does not accept that diagnostic geometry as a normal result. Successful Mapbox responses are cached by a SHA-256 request key under .cache/mapbox-directions/; Nominatim results use .cache/nominatim.json.
The tests preserve the failures that motivated those rules. test_mapbox.py checks that 26 anchors become (0..24) and (24..25). test_scoring.py makes a known-corridor candidate beat a far detour, while test_regressions.py keeps a valid alternative when the top path fails the detour check. The tests are the reason this page describes rejection as behavior rather than as an aspiration.
Scoring is a review queue
The score combines corridor coverage, known-route overlap, family similarity, length plausibility, snap quality, and backtrack quality. The hard QA layer rejects empty geometry, routes shorter than 300 metres, anchors beyond their snap limits, non-finite coordinates, and detour ratios above 6. It warns about high U-turn ratios and multi-request routes so a reviewer can see where the candidate is fragile.
The status decision is intentionally conservative:
# One candidate cannot auto-accept: there is no alternative to compare.if ( len(scored) > 1 and score >= config.scoring.auto_accept_score and margin >= config.scoring.auto_accept_margin and _has_accept_evidence(candidate, metrics)): status = "auto-accept"elif score >= config.scoring.review_score: status = "review"else: status = "low-confidence"The export code says what canonical means in this context: auto-accepted candidates only. Review and low-confidence candidates remain in the combined GeoJSON and report. The current production jeepney bundle is still marked unreviewed, and the inference tool has no checked-in output report in this checkout.
The repository also has no checked-in screenshot of the review UI or a before-and-after inference map. When a run is available, the baseline and candidate GeoJSON files are the inspectable map artifacts. This page does not invent a visual result for a run that is not present.
The build, local review UI, and tests live under packages/transit/tools/route-inference. The production distinction is covered in jeepney route data and inference.