Tell it the ride you want — "a flat 20-mile loop near the water, mostly protected" — and it returns real routes on a map, ranked, each with a line on why it fits.
Generic mapping tools answer "how do I get from A to B." They can't answer "find me a good ride" — there's no destination to route to, and "good" isn't something you can filter on.
So the problem becomes search, not navigation. Build a library of real, scored rides from open civic data; let a model read the request, find the closest matches by meaning, and say why each fits — on a map, across several metro regions.
The corpus pipeline. This half never serves a request — its only job is to build a clean set of rides to search. It's an isolated Python 3.12 + uv project that shares nothing with the serving app but the PostGIS database itself; ADR-0001 sets that boundary on purpose, so the two halves evolve on their own.
It ingests rides from several open-data sources — OSM route relations via Overpass, NYSDOT routes via ArcGIS, NYC DOT bike facilities via Socrata, USBRS and hand-curated "canon" rides, and openrouteservice-generated loops — then map-matches every geometry through Valhalla (snapping noisy GPS traces onto the real road network), resampling each line to a stable spacing so the snap stays clean and recording a per-route match-quality fraction. A tunable heuristic then scores each route on protected-lane coverage, greenway fraction, surface quality, and a per-source trust prior — weights and gates living in config, not code. Because the same physical ride often arrives from two feeds, a full-table dedup pass reconciles them: an ADR-documented source-precedence ladder (ADR-0003) keeps the higher-trust copy, hard-deletes the loser, and writes an audit row for every removal. Finally it composes a deterministic text description of each route and embeds it with OpenAI text-embedding-3-small, keyed on a content hash so re-runs only re-embed what actually changed.
Two correctness habits run through all of it. Routes are stored in WGS-84, but every metric computation — lengths, buffers, overlaps, radius filters — projects to UTM-18N first (ADR-0002), so distances are real metres rather than degrees. And every external API response is disk-cached, so a run that exhausts a daily API quota resumes the next day for free.
The serving app. A TypeScript monorepo — shared types, an Express + Drizzle server, a React client — reads the corpus read-only.
A search is four steps. An LLM pulls the hard constraints from the request — distance, loop or not — as structured output. The query is embedded with the same model that built the corpus, and pgvector finds the nearest routes by cosine distance in the active region. A second LLM call re-ranks the shortlist and writes a line on why each fits.
Most of the effort goes into keeping those steps honest. The app refuses to start if the corpus and the query were embedded by different models. A re-rank that invents or repeats a route ID is discarded, and the retrieval order stands. When the filters match nothing, the search loosens them and says so, instead of returning rides that don't fit.
A separate LangGraph agent (gpt-4.1-mini) plans one-off rides through geospatial tools over MCP, streaming text and maps over SSE. Leave mid-answer and it stops generating — nothing is spent finishing a reply no one is waiting for. The React + MapLibre client renders the map, facilities, and points of interest. Underneath it: written standards, five ADRs, and tests on every commit.
Stack Reference
- Serving app: TypeScript (strict ESM) · npm workspaces monorepo (
shared/server/client) · Express · Drizzle ORM · pg/PostGIS · pgvector · Zod - AI / search: LangChain v1 · LangGraph (ReAct agent) ·
@langchain/openai·@langchain/mcp-adapters(Magic Lane geospatial tools) · OpenAIgpt-4.1-mini(constraint extraction, re-rank, chat) · OpenAItext-embedding-3-small(1536-dim) ·@langchain/anthropicadapter wired in - Client: React 18 · Vite · Tailwind v4 (CSS-first) · MapLibre GL + react-map-gl · react-router 7
- Corpus pipeline: Python 3.12 · uv · PostGIS · Shapely · pyproj · httpx · Typer · Pydantic · Valhalla (map-matching) · openrouteservice / Overpass / ArcGIS / Socrata clients
- Data / infra: Neon Postgres · disk-cached HTTP for resumable runs · multi-region partition (NYC, Seattle)
- Engineering: Vitest + Supertest · ESLint · husky + lint-staged ·
cigate (lint → typecheck → test) · STANDARDS.md · 5 ADRs · custom Claude Code project skills
