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

4.7 KiB
Raw Blame History

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
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:

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 gateisPointInVietnam(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 Overpasskubectl 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 stateOsmSyncOrchestrator 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