A Places explorer for live OpenStreetMap data.
Filter by industry, brand, place name, geography, and optional OSM tags. Browse results on a Mapbox map and in a side panel. Stream Overpass attempt progress while a search runs. Export place geometry (point, polygon, or multipolygon) as CSV.
Nominatim and Overpass stay behind a Node BFF that uses only Node builtins plus shared places-core (no Express, Zod, or other third-party runtime deps). The browser talks only to that API (and Mapbox for the map).
Browser (Vite React SPA) ──► Places API (Node BFF) ──► Nominatim
│ │
└──► Mapbox GL └──► Overpass (mirrored)
╲ ╱
places-core
| Package | Role |
|---|---|
apps/web |
Vite React SPA — filters, Mapbox map, results, geometry export |
apps/api |
Node HTTP BFF — validate, geocode, Overpass, NDJSON progress, RFC 9457 errors |
packages/places-core |
Shared types, taxonomy, full ISO country list, OSM/Overpass constants |
Deeper module maps and data flow: .agents/docs/ARCHITECTURE.md.
- Node.js
>=22 - pnpm
11.8.0(pinned viapackageManagerin rootpackage.json)
corepack enable
pnpm installcp apps/web/.env.example apps/web/.env
cp apps/api/.env.example apps/api/.env| App | Required | Notes |
|---|---|---|
| Web | VITE_MAPBOX_GL_JS_PUBLIC |
Public Mapbox token (pk.*). Restrict origins in the Mapbox token UI — include http://localhost:5173 for local dev. |
| Web | VITE_API_BASE_URL |
API origin, no trailing slash (default http://localhost:8787). |
| API | NOMINATIM_USER_AGENT |
Identifying User-Agent for Nominatim policy. |
| API | NOMINATIM_EMAIL |
Reachable contact email — placeholders like example.com often get HTTP 403. |
| API | CORS_ORIGINS |
Comma-separated browser origins (defaults cover common Vite ports). |
Optional API knobs (rate limits, Overpass mirrors, listen host/port) are documented in apps/api/.env.example.
pnpm dev| Process | URL |
|---|---|
| Places API | http://localhost:8787 |
| Vite app | http://localhost:5173 |
- Run one side alone:
pnpm dev:apiorpnpm dev:web. - If Vite picks a different port because
:5173is busy, add that origin toCORS_ORIGINS(or free the stale process). - Missing Nominatim env usually means
apps/api/.envwas never created from the example.
pnpm build # places-core → api → web
pnpm preview # Vite preview of apps/webpnpm test # Vitest: core, api, web
pnpm check # Biome format + lint
pnpm doctor:full # React Doctor on the web appCI (see .github/workflows/):
test.yml— Vitest + production buildlint.yml— Biome CI, lockfile check, React Doctoraudit.yml— dependency audit
| Script | Description |
|---|---|
pnpm dev |
API + web in parallel |
pnpm dev:api / pnpm dev:web |
Single process |
pnpm build |
Build core → api → web |
pnpm test |
Vitest across the workspace |
pnpm check |
Biome check (format + lint) |
pnpm format / pnpm lint |
Format or lint only |
pnpm doctor / pnpm doctor:full |
React Doctor |
pnpm preview |
Preview the web production build |
| Filter | Behavior |
|---|---|
| Category / subcategory | Curated OSM industry taxonomy from places-core |
| Brand | Plain text; exact case-insensitive match on OSM brand |
| Place name | Substring / contains |
| Country | Searchable full ISO 3166-1 alpha-2 list (API accepts any 2-letter code) |
| State / region, city | Resolved via Nominatim for Overpass spatial scope |
| Advanced OSM tag | Allowlisted top-level OSM feature keys + exact value |
A search needs at least one of: category, brand, name, or OSM tag (geography alone is not enough). Results are capped at RESULT_LIMIT (2500); the UI surfaces truncation when Overpass returns a full page.
While Overpass runs, the SPA requests NDJSON (Accept: application/x-ndjson) so the API can stream mirror/attempt progress before the final result. Geometry export re-queries through POST /places/export — never Overpass from the browser.
- Admin areas / place search: public Nominatim and Overpass via
apps/apionly — not from the browser - Map: Mapbox GL JS (light style)
Place data © OpenStreetMap contributors (ODbL). Map display © Mapbox / OpenStreetMap.
- Brand is exact, not fuzzy. There is no brand autocomplete catalog. Type the OSM
brandtag as it appears in the data. - Category labels are first-match. If an element matches more than one taxonomy entry, the first match in the bundled list wins — not a scored “best” industry.
- Country UI vs API. The dropdown is a full static ISO list from
places-core; the API still accepts any ISO alpha-2 code. - Co-versioned DTOs. The SPA casts Places API success JSON to
places-coretypes with only a light shape check. Deployapps/webandapps/apifrom the same revision. - Public OSM capacity. Searches depend on public Nominatim/Overpass fairness. Identify yourself with a real User-Agent and email; expect rate limits and occasional upstream failures.
- Retail-area export coverage. Advanced export “Include Retail Area” only finds enclosing
landuse=retail/shop=mall/landuse=commercialpolygons present in OSM for the search scope; otherwise the place footprint is kept. - Hosting. Local Node + Vite is first-class. Long Overpass wall-clock budgets fit long-running Node/Containers better than short-lived Workers.
| Doc | Purpose |
|---|---|
AGENTS.md |
Agent orientation, skills, commands |
.agents/docs/ARCHITECTURE.md |
System architecture |
.agents/skills/ |
Package and workflow skills |
MIT — see LICENSE.