feat(transit): let an admin route transit search through Google - #2224
Merged
Conversation
Transitous has no GTFS coverage for much of Asia, so a search between two Osaka subway stops finds nothing — the stops are not in the feed at all. Add Google as an optional backend behind an admin setting, reusing the install's existing Places key, and fall back to Transitous whenever no key resolves. Uses the Routes API rather than the legacy Directions endpoint the request named: every other Google call here already speaks the v1/X-Goog-Api-Key family, and Google closed the legacy endpoint to Cloud projects created after March 2025. Google returns walking as turn-by-turn navigation steps where MOTIS returns one leg per walking segment, so consecutive walk steps are coalesced into a single leg — otherwise a finely-sliced walk pushed a real journey past the 20-leg cap and the whole itinerary was dropped. Both responses now name the backend that answered. The fallback is silent by design, which left an empty result ambiguous between "no data here" and "the provider you picked never ran", and the admin panel warns when Google is selected but no key resolves — or when only the admin's own key is set, which serves them and nobody else. Cost shaped the implementation, since Google bills where Transitous does not: both field masks are cut to the cheapest SKU tier that still answers the question, cache TTLs are longer than the Transitous path's, and route alternatives ride along inside the already-billed request. Addresses #1699
|
Collaborator
|
I think you need to let Claude know not to include any more line breaks in PRs. 😅 |
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Description
Transitous has no GTFS coverage for much of Asia — a search between two Osaka
subway stops returns "No routes found" because the stops are not in the feed at
all. This adds Google as an optional transit backend behind an admin setting.
Admins pick the backend in Admin → Settings → Transit Provider. Google reuses
the Places key the install already has (
maps_api_key: operator env → instance →the caller's own row), so there is nothing new to configure. Transitous stays the
default and the fallback: an install that never opens the switch, or that selects
Google before pasting a key, keeps running on Transitous.
Routes API, not the legacy Directions API that the discussion named. Every
other Google call in this repo already speaks the v1/
X-Goog-Api-Keyfamily, andGoogle closed the legacy endpoint to Cloud projects created after March 2025 — a
new self-hoster cannot enable it.
Layers:
shared/src/admin/admin.schema.ts:TRANSIT_PROVIDERS, the PUTbody schema, and
ApiKeySource, which the server'sinstance-api-keys.tsnowaliases rather than restating (it goes on the wire, so two copies would fork it).
server/src/nest/transit/transit-provider.ts: oneapp_settingsrow, read by
TransitServiceper request, written byAddonsService. Readsfail-safe — a missing row or an unrecognised value resolves to Transitous
rather than silently billing a key. No migration needed.
server/src/nest/transit/google-transit.provider.ts: Routes APIfor
plan, Places Text Search forgeocode, mapped onto the existing compactitinerary shape.
transit.service.ts: after validation, so both backends are heldto the same coordinate/mode/transfer contract and return the same 400s.
GET/PUT /api/admin/transit-provider, audited.AdminSettingsTab.tsx; seven keysacross all 23 locales.
Three things worth a reviewer's attention:
Walking is reshaped. Google returns walking as turn-by-turn navigation steps
where MOTIS returns one leg per walking segment, so consecutive walk steps are
coalesced into one leg. Without it a two-minute walk arrived as six legs, and a
journey with sliced walks at both ends exceeded
transitItinerarySchema's 20-legcap and was dropped whole at the MCP boundary. Merging the geometry needs an
encoder (step polylines are delta-encoded from their own first point and cannot
be concatenated), so
transit.helpers.tsgains a polyline codec.Responses name the backend that answered. The fallback is silent by design,
which left an empty result ambiguous between "this provider has no data here" and
"the provider you picked never ran". The panel's empty state now reads "No
connections found via Transitous."
{provider}is a placeholder in every locale,never a baked-in name — hardcoding it per locale is the #1611 bug class, and
i18n-placeholders.spec.tsenforces it.The admin panel warns about keys. Selecting Google with no key that resolves
changes nothing, so the panel says so. It also warns when only the admin's own
key is set: the resolver's last step is the caller's own row, so a personal key
serves that admin while every other member silently gets Transitous — the #1939
shape one layer up.
Cost shaped the implementation, since Google bills where Transitous does not. Both
field masks are cut to the cheapest SKU tier that still answers the question — the
station picker deliberately does not reuse
SEARCH_TEXT_FIELD_MASK, whoserating/website/phone fields bill Text Search at Enterprise. Cache TTLs are longer
than the Transitous path's 60s, and route alternatives ride along inside the one
request that was already billed.
Related Issue or Discussion
Addresses discussion #1699
Type of Change
Checklist
devdevbranch, notmain