Davao gives a trip planner an uncomfortable shortcut. Two services can pass within a few hundred metres and still have no useful transfer, while a graph can find a mathematically valid path that asks a rider to walk across most of the city before boarding. The planner therefore has two jobs: preserve the transit network supported by available evidence, then remove results that are technically connected but poor advice.

The second job became concrete in the Buhangin to Sampaguita case. The origin was longitude 125.62562, latitude 7.11925. The historical incident record described a 22.2-kilometre trip with a 4.7-kilometre initial access walk, a 17-kilometre ride, and about 5.2 kilometres of walking in total. The current profile script still carries this query as the buhangin-southbound-regression scenario, and the regression suite keeps the same pins. It does not assert those historical distances, so this page treats the measurements as incident evidence rather than a current output guarantee.

The rule that came out of that case is simple: search validity is not recommendation quality. The graph needs to know whether an edge is legal under the transit model. A separate selection step needs to decide whether the resulting itinerary is acceptable to a person.

From two places to a trip

The browser sends the planner two coordinates, a mode, and names for display. Place search is a separate concern. A geocoder may turn “Sampaguita Street” into a coordinate, but it does not add a stop, a route, or a transfer to the transit graph.

The planner first handles the cheap decisions. If the pins are within 25 metres, the result is samePlace. If either pin falls outside the compiled service area, the result names that place as outsideServiceArea. Otherwise, the planner enables bus, jeepney, or both services and projects each endpoint onto the enabled network.

Projection is not a new transit stop. It creates temporary access and egress edges from the query pins to nearby service geometry. The planner also considers service coverage endpoints, because the nearest point on a line is not always the best place to enter or leave a directed service. The distance is geometric. The current implementation does not route the access walk around crossings, barriers, or private land.

From there, the engine searches the directed graph in reverse, starting at the destination. It records candidates rather than committing immediately to one answer. Selection then applies the rider rules: each end walk is at most 1,500 metres, the two end walks together are at most 2,000 metres, an itinerary has no more than two rides, and a service cannot be boarded again after a transfer. The selected candidates are materialized into boarding, alighting, walking, and transfer instructions.

The order matters. Applying the walking cap while the graph is being compiled would discard the same transit service for every possible origin. Applying it after projection lets the compiler keep the network reusable while the query decides whether a particular access walk is acceptable.

Which connections count

Bus and jeepney data do not carry the same authority. A bus service is compiled from an ordered stop sequence. A jeepney service carries an explicit direction and its route geometry, but it does not get a synthetic reverse direction unless the source direction data requests one.

Same-mode transfers use a shared transferKey. The compiler can connect nodes in that group when they belong to different services of the same mode. A bus-to-jeepney transfer follows a different rule. The bus endpoint must be an official bus stop, and the two endpoints must be within 400 metres by the planner's coordinate-distance calculation. A nearby arbitrary point on a bus line is rejected during compilation.

This is conservative, but it is not a blanket ban on geometric transfers. It permits a typed, bounded intermodal edge and rejects an untyped “these lines look close” connection. That distinction is why the current implementation checks stopOrdinal before it measures the distance.

The selector checks the same boundary again after it has reconstructed the ride groups. This is a partial excerpt from that check:

example.ts
1 for (const [index, ride] of rides.entries()) {
2 const serviceId = graph.serviceIds[ride.serviceOrdinal]
3 if (serviceId === undefined) throw new Error("candidate service is missing")
4 if (seenServices.has(serviceId)) return false
5 seenServices.add(serviceId)
6 if (index > 0 && ride.transferWalkMm > TRANSFER_MAX_MM) return false
7 }

The source is planner-select.ts, in the current candidateHasValidRideSequence implementation. Compilation prevents unsupported transfer edges from entering the graph; selection checks the complete candidate again before it becomes a rider-facing trip. The selector also prevents reboarding the same service. The compiler's official-stop guard is shown in ADR 0005.

The corresponding tests cover both sides of the boundary. A transfer at 400 metres compiles and appears in the resulting itinerary. A transfer at 401 metres does not. The test is in planner-compiler.test.ts; it also checks that the resulting transfer walk remains within the cap.

The search cost is expressed in planner ticks. One tick is 200,000 microseconds. Ride edges use 1,000 millimetres per tick, walking edges use 250 millimetres per tick, and access edges add mode-specific boarding waits. Those values let the search compare paths, but they do not encode every product decision.

Selection ranks candidates by score, ride count, transfer walking, total walking, longest walking leg, ride distance, service ID, and a stable identity. It keeps the best candidate for each first service, suppresses near duplicates when their endpoints are within 100 metres and their scores are within two minutes, and returns at most four results. In both mode, the current selector gives priority to a candidate that uses a jeepney when one survives the policy filters. That is a diversity rule in the result set, not a claim that jeepney is always faster.

The endpoint rule is implemented by accessWithinCaps in planner-runtime-policy.ts. The selection layer applies the same rule to a complete candidate, then checks its ride sequence. There is no current MIN_TRANSIT_RIDE_MM contract constant. Zero-length ride segments are rejected during compilation, and a candidate with no ride distance is rejected while candidates are built. The decision record shows the small policy function and its placement in the runtime boundary.

What the browser actually runs

The browser does not rebuild the network from JSON for every query. Build time produces a versioned .wayplan image. The web app fetches its manifest and content-addressed image, verifies the byte length and SHA-256 digest, then transfers the image buffer to a dedicated worker. The worker verifies it again, mounts it, and handles planning requests without using the main UI thread.

The current web client sends liveBusArrivals: [] with the planning request. Live bus arrivals therefore do not currently alter the browser planner result through this path. They are an optional enrichment boundary, not a prerequisite for static route planning. Fares are also outside the current graph score. They may be shown in a trip result when the product has fare data, but they do not establish connectivity.

The artifact and worker lifecycle are explained in the planner engine, the image build, and the offline cache experiment. The two events that changed the planner's shape are the absurd-route postmortem and the server-to-client pivot.

Limits that remain

The 1,500-metre rule is a product boundary, not a pedestrian network. A 1,499-metre straight-line projection can still require a longer walk on the street. The 400-metre transfer rule has the same limit: it controls what the graph may connect, but it does not prove that a rider can cross every road between the endpoints.

The planner also depends on the quality of the compiled service data. An explicit jeepney direction can still describe an imperfect route, and a bus stop identity can still drift in upstream data. Those are data-pipeline problems. The planner's job is to preserve the distinctions it has evidence for and avoid silently inventing the rest.