Skip to content

feat(transit): let an admin route transit search through Google - #2224

Merged
mauriceboe merged 1 commit into
devfrom
feat/google-transit-backend
Sep 7, 2026
Merged

feat(transit): let an admin route transit search through Google#2224
mauriceboe merged 1 commit into
devfrom
feat/google-transit-backend

Conversation

@subdee

@subdee subdee commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

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-Key family, and
Google closed the legacy endpoint to Cloud projects created after March 2025 — a
new self-hoster cannot enable it.

Layers:

  • Contractshared/src/admin/admin.schema.ts: TRANSIT_PROVIDERS, the PUT
    body schema, and ApiKeySource, which the server's instance-api-keys.ts now
    aliases rather than restating (it goes on the wire, so two copies would fork it).
  • Settingserver/src/nest/transit/transit-provider.ts: one app_settings
    row, read by TransitService per request, written by AddonsService. Reads
    fail-safe — a missing row or an unrecognised value resolves to Transitous
    rather than silently billing a key. No migration needed.
  • Backendserver/src/nest/transit/google-transit.provider.ts: Routes API
    for plan, Places Text Search for geocode, mapped onto the existing compact
    itinerary shape.
  • Dispatchtransit.service.ts: after validation, so both backends are held
    to the same coordinate/mode/transfer contract and return the same 400s.
  • AdminGET/PUT /api/admin/transit-provider, audited.
  • UI + i18n — a select and two warnings in AdminSettingsTab.tsx; seven keys
    across 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-leg
cap 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.ts gains 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.ts enforces 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, whose
rating/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

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Checklist

  • I have read the Contributing Guidelines
  • My branch is up to date with dev
  • This PR targets the dev branch, not main
  • I have tested my changes locally
  • I have added/updated tests that prove my fix is effective or that my feature works
  • I have updated documentation if needed

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
@subdee
subdee requested review from jubnl and mauriceboe September 3, 2026 16:44
@subdee subdee self-assigned this Sep 3, 2026
@sonarqubecloud

sonarqubecloud Bot commented Sep 3, 2026

Copy link
Copy Markdown

@mauriceboe

Copy link
Copy Markdown
Collaborator

I think you need to let Claude know not to include any more line breaks in PRs. 😅

@mauriceboe mauriceboe left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@mauriceboe
mauriceboe merged commit 2a0ceb2 into dev Sep 7, 2026
15 checks passed
@mauriceboe
mauriceboe deleted the feat/google-transit-backend branch September 7, 2026 07:02
@mauriceboe mauriceboe mentioned this pull request Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants