Files
goodgo-platform/docs/osm-data-model.md
Ho Ngoc Hai a9770a5f93 feat(osm): user-facing UX — POI sidebar + search filter + docs
* Listing detail: drop the new <NearbyPoiSidebar> below the price card
  with default 1.5km radius and 6 categories (school/secondary/hospital/
  market/bank/metro). Reads property.lat/lng — no-op when unset.
* KCN detail: same component but 3km radius with the categories that
  matter for industrial parks (hospital/bank/gas/bus/metro/police).
* New <PoiSearchFilter> widget for the search page: pill button →
  popover with radius dropdown (300m..5km), 3 quick presets ("Family",
  "Commute", "Convenience"), and 6 grouped category checkboxes. Wires
  to a `PoiNearbyConstraint` value so callers can pass it into search
  filters when they're ready.
* docs/osm-data-model.md: canonical reference for every OSM-sourced
  table, sync cadence, quality gates, runbook for ops, and a clear
  "how to add a new POI category" guide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:06:52 +07:00

93 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# OSM Data Model — GoodGo Platform
This document is the canonical reference for every OpenStreetMap-sourced
table in the GoodGo database, the sync pipelines that populate them, and
the query patterns that use them.
## Tables at a glance
| Table | Source | Geometry | Sync cadence | Used by |
|-------|--------|----------|--------------|---------|
| `vn_provinces` | OSM `boundary=administrative + admin_level=4` | MultiPolygon | Weekly (Mon 02:30 ICT) | `GeoLookupService`, KCN sync, address auto-fill |
| `vn_districts` | OSM `admin_level=6` | MultiPolygon | Weekly (Wed 02:30 ICT) | Same as above |
| `vn_wards` | OSM `admin_level=8` | MultiPolygon | Weekly (Sat 02:30 ICT) | Same as above |
| `Poi` | OSM nodes/ways/relations matching 20 category selectors | Point | Daily 1 category rotation (02:00 ICT) | `/poi/nearby`, `/poi/by-bbox`, listing sidebar, search filter |
| `TransportLine` | OSM `route=subway|train|highway` relations | MultiLineString | Monthly | Distance scoring, planned for Phase 2 UX |
| `IndustrialPark` | OSM `landuse=industrial` ways/relations | Point + MultiPolygon boundary | Monthly (1st 03:00 ICT, 4 chunks) | `/industrial/parks/*`, KCN catalog |
| `OsmSyncRun` | Generated by orchestrator | — | Append-only audit | `/admin/osm` dashboard |
All sync writes are gated by `OSM_SYNC_ENABLED=true` so dev / staging
environments don't hit Overpass accidentally.
## GeoLookupService — the foundation
Every other layer depends on `vn_provinces.geometry` for PostGIS
`ST_Contains` lookups. The service exposes:
```ts
const r = await geo.lookup(lng, lat);
// → { province: { code, name }, district: { code, name }, ward: { code, name } }
const inside = await geo.isInVietnam(lng, lat);
// → boolean
const cov = await geo.coverage();
// → { provinces: { total, withGeometry, lastSyncedAt }, districts: ..., wards: ... }
```
It replaces the old `nearestProvince()` heuristic that walked a
hardcoded centroid table.
## Quality gates baked into sync scripts
1. **Geographic gate**`isPointInVietnam(lng, lat)` from
`scripts/data/vn-country-polygon.ts` rejects rows whose centroid
falls outside the VN mainland polygon (catches China / Laos /
Cambodia bleed across the Overpass bbox chunks).
2. **Name gate** — rows whose `name` contains zero Latin/Vietnamese
letters (`/[A-Za-zÀ-ỹ]/`) are dropped (filters CJK / Khmer / Thai).
3. **Lock gate** — when an admin sets `osmLocked=true` or adds a column
to `lockedFields`, the next sync skips that row entirely (or that
column) so manual edits survive.
## Adding a new POI category
1. Add the enum value to `PoiCategory` in `prisma/schema.prisma` and
create a Prisma migration that `ALTER TYPE "PoiCategory" ADD VALUE`.
2. Add the Overpass selector to `CATEGORY_QUERIES` in
`scripts/sync-osm-poi.ts`.
3. Append the same enum value to the `POI_CATEGORIES` rotation list in
`OsmSyncCronService` so the cron picks it up.
4. Add labels + icons + colour to `apps/web/lib/poi-api.ts` so the UI
chips render.
That's it — `OsmSyncService.findLayer('poi', 'YOUR_CAT')` will return a
def automatically because `SYNC_LAYERS` is generated from the enum keys.
## Operational runbook
* **Sync hangs / 504 from Overpass** — `kubectl describe pod` on the
Kaniko-style sync runner shows the chunk in flight. The script has
a 5× retry on the clone step (HTTP 504 from Gitea is transient).
For Overpass itself, raise the per-script `[out:json][timeout:N]`
by editing the script. Default 180s for POI, 300s for boundaries.
* **Runs stuck in `RUNNING` state** — `OsmSyncOrchestrator` writes the
row before spawning the script. If the script process dies without
emitting an `exit` event, the row stays RUNNING. Mitigation: cron
job to flip RUNNING > 6h old to FAILED with `errorMessage='timeout'`.
* **Conflict logs** — when sync updates a column the admin had locked,
it skips the column silently. There is no separate conflict table
(yet). To audit, search Loki for `[osm-sync] skipping locked field`.
## Phase status
| Phase | Status | Notes |
|-------|--------|-------|
| 0 — Admin boundaries + GeoLookupService | ✅ Schema, sync, service done. Provinces synced (33), districts in progress |
| 1 — POI catalog + sync | ✅ Schema + sync script + NestJS module + sidebar component done. Hospital category synced (~500 rows) |
| 2 — Transport (metro/railway/airport) | 🟡 Stations synced via POI; lines layer pending |
| 3 — Buildings / landuse | ⏳ Deferred — admin says low priority |
| 4 — Sync orchestrator + admin dashboard | ✅ Service + cron + Prometheus-friendly stats + admin UI done |
| 5 — User-facing UX | 🟡 Listing + KCN sidebar wired; search filter widget built; map overlays pending |
| 6 — Performance hardening | ⏳ Materialized views + Redis cache pending |