Files
goodgo-platform/scripts/data/vn-province-centroids.ts
Ho Ngoc Hai c15bdcc6bf
Some checks failed
CI / E2E Tests (push) Has been skipped
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 9s
CI / AI Services (Python) — Smoke (push) Failing after 7s
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 1m7s
Deploy / Build API Image (push) Failing after 16s
Deploy / Build Web Image (push) Failing after 6s
Deploy / Build AI Services Image (push) Failing after 7s
E2E Tests / Playwright E2E (push) Failing after 15s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 5s
Security Scanning / Trivy Scan — API Image (push) Failing after 1m13s
Security Scanning / Trivy Scan — Web Image (push) Failing after 49s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 40s
Security Scanning / Trivy Filesystem Scan (push) Failing after 40s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Security Scanning / Security Gate (push) Failing after 1s
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
fix(industrial): improve OSM review UX + public map visibility
Four UX issues surfaced when reviewing the new OSM-sync pipeline against
the actual 2,193 imports — fixed in this commit:

1. Admin queue surfaced noise first.
   `ListOsmPendingHandler` now sorts by `totalAreaHa DESC` (real KCN
   first, single-factory `landuse=industrial` polygons last) and accepts
   `minAreaHa` (default 50 ha) plus a `region` filter. The admin page
   exposes both as dropdowns — "Tất cả / ≥ 5 / ≥ 50 / ≥ 200 / ≥ 500 ha".
   Top-of-queue is now Bàu Bàng (2,597 ha) and Nhơn Trạch (2,535 ha).

2. Promote dialog said "KCN KCN Đại An" — duplicate prefix.
   Reworded to "Sắp promote: <name>" so the row name stands on its own.

3. Province was "Chưa xác định" on 2,107 of 2,193 OSM rows.
   The OSM tags lacked any addr:* hint, so the importer never had
   anything to write. Added `scripts/data/vn-province-centroids.ts` (63
   provinces with capital-city coords) and a `nearestProvince(lat, lng)`
   fallback in `parseFeature()`. Shipped a one-shot backfill script
   `scripts/backfill-osm-provinces.ts` and ran it — every existing OSM
   row now has a province (Hồ Chí Minh: 408, Lạng Sơn: 232,
   Quảng Ninh: 220, Hà Nội: 172, Hải Phòng: 105, …). Admin can correct
   on promote if the nearest-centroid heuristic picked the wrong
   neighbour for a long-thin province.

4. Public map looked empty — only 20 curated parks visible.
   Added an opt-in toggle "Hiển thị KCN OSM" with a small legend above
   the map. When on, the bbox endpoint returns OSM raw rows too; markers
   render in amber (vs. green for curated) at slightly smaller radius
   and lower opacity, so the visual hierarchy stays clear. Refetch is
   wired through a ref so the toggle takes effect without remounting
   the map.

Verified in browser preview: zoom-out shows clusters of 320 / 71 / etc.
across the country with the toggle on, and just three small clusters
(20 curated parks) when off.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 00:09:24 +07:00

108 lines
5.1 KiB
TypeScript

/**
* Approximate centroids for Vietnam's 63 provinces / centrally-administered
* cities, used as a fallback when OSM `addr:state` / `addr:province` tags
* are missing (which is the common case for industrial-park polygons in VN).
*
* The coords are public-domain capital-city centroids (good enough for a
* "which province does this point land in?" nearest-neighbour lookup at
* the +/- few-km level — the long shape of some provinces means the
* nearest centroid will occasionally pick the wrong neighbour, but admin
* can correct on promote.
*
* Province name canonical form = display name in Vietnamese (no diacritics
* stripped) so it matches what the seed data + UI already uses.
*/
export interface ProvinceCentroid {
name: string;
lat: number;
lng: number;
}
export const VN_PROVINCE_CENTROIDS: ProvinceCentroid[] = [
{ name: 'Hà Nội', lat: 21.028511, lng: 105.804817 },
{ name: 'Hồ Chí Minh', lat: 10.762622, lng: 106.660172 },
{ name: 'Hải Phòng', lat: 20.844912, lng: 106.688084 },
{ name: 'Đà Nẵng', lat: 16.054407, lng: 108.202167 },
{ name: 'Cần Thơ', lat: 10.045162, lng: 105.746857 },
{ name: 'An Giang', lat: 10.521583, lng: 105.125896 },
{ name: 'Bà Rịa - Vũng Tàu', lat: 10.541136, lng: 107.242851 },
{ name: 'Bạc Liêu', lat: 9.294085, lng: 105.721787 },
{ name: 'Bắc Giang', lat: 21.281932, lng: 106.197658 },
{ name: 'Bắc Kạn', lat: 22.147291, lng: 105.834160 },
{ name: 'Bắc Ninh', lat: 21.186080, lng: 106.076371 },
{ name: 'Bến Tre', lat: 10.243519, lng: 106.375140 },
{ name: 'Bình Dương', lat: 11.325237, lng: 106.477017 },
{ name: 'Bình Định', lat: 13.782250, lng: 109.219574 },
{ name: 'Bình Phước', lat: 11.751888, lng: 106.723917 },
{ name: 'Bình Thuận', lat: 11.090668, lng: 108.072094 },
{ name: 'Cà Mau', lat: 9.176790, lng: 105.150253 },
{ name: 'Cao Bằng', lat: 22.665684, lng: 106.257549 },
{ name: 'Đắk Lắk', lat: 12.710017, lng: 108.237633 },
{ name: 'Đắk Nông', lat: 12.264144, lng: 107.609794 },
{ name: 'Điện Biên', lat: 21.386073, lng: 103.016510 },
{ name: 'Đồng Nai', lat: 11.066000, lng: 107.166700 },
{ name: 'Đồng Tháp', lat: 10.493333, lng: 105.688200 },
{ name: 'Gia Lai', lat: 13.808078, lng: 108.109375 },
{ name: 'Hà Giang', lat: 22.823453, lng: 104.978573 },
{ name: 'Hà Nam', lat: 20.541127, lng: 105.913303 },
{ name: 'Hà Tĩnh', lat: 18.342745, lng: 105.905499 },
{ name: 'Hải Dương', lat: 20.940981, lng: 106.330156 },
{ name: 'Hậu Giang', lat: 9.757897, lng: 105.641110 },
{ name: 'Hòa Bình', lat: 20.817192, lng: 105.337720 },
{ name: 'Hưng Yên', lat: 20.852241, lng: 106.015511 },
{ name: 'Khánh Hòa', lat: 12.258515, lng: 109.052528 },
{ name: 'Kiên Giang', lat: 10.012103, lng: 105.080921 },
{ name: 'Kon Tum', lat: 14.349953, lng: 108.000534 },
{ name: 'Lai Châu', lat: 22.396519, lng: 103.471370 },
{ name: 'Lâm Đồng', lat: 11.940419, lng: 108.458313 },
{ name: 'Lạng Sơn', lat: 21.853708, lng: 106.761525 },
{ name: 'Lào Cai', lat: 22.485198, lng: 103.974845 },
{ name: 'Long An', lat: 10.535359, lng: 106.405460 },
{ name: 'Nam Định', lat: 20.438822, lng: 106.162106 },
{ name: 'Nghệ An', lat: 18.679585, lng: 105.681753 },
{ name: 'Ninh Bình', lat: 20.250716, lng: 105.974623 },
{ name: 'Ninh Thuận', lat: 11.673911, lng: 108.864225 },
{ name: 'Phú Thọ', lat: 21.398716, lng: 105.151787 },
{ name: 'Phú Yên', lat: 13.088067, lng: 109.092876 },
{ name: 'Quảng Bình', lat: 17.478366, lng: 106.621288 },
{ name: 'Quảng Nam', lat: 15.539353, lng: 108.019047 },
{ name: 'Quảng Ngãi', lat: 15.120137, lng: 108.804340 },
{ name: 'Quảng Ninh', lat: 21.006382, lng: 107.292514 },
{ name: 'Quảng Trị', lat: 16.745360, lng: 107.187462 },
{ name: 'Sóc Trăng', lat: 9.602522, lng: 105.973797 },
{ name: 'Sơn La', lat: 21.328228, lng: 103.914400 },
{ name: 'Tây Ninh', lat: 11.310300, lng: 106.098046 },
{ name: 'Thái Bình', lat: 20.446016, lng: 106.336445 },
{ name: 'Thái Nguyên', lat: 21.594280, lng: 105.844866 },
{ name: 'Thanh Hóa', lat: 19.806692, lng: 105.785182 },
{ name: 'Thừa Thiên Huế', lat: 16.467397, lng: 107.590533 },
{ name: 'Tiền Giang', lat: 10.449334, lng: 106.342120 },
{ name: 'Trà Vinh', lat: 9.812741, lng: 106.299250 },
{ name: 'Tuyên Quang', lat: 21.778812, lng: 105.218015 },
{ name: 'Vĩnh Long', lat: 10.253938, lng: 105.972456 },
{ name: 'Vĩnh Phúc', lat: 21.308690, lng: 105.604813 },
{ name: 'Yên Bái', lat: 21.722733, lng: 104.911289 },
];
/**
* Find the nearest province centroid for a given lat/lng. Uses naive squared
* distance — fine for VN's ~1500-km north-south extent. Always returns a
* province (the dataset covers the country); callers can fall back to
* "Chưa xác định" if they need to.
*/
export function nearestProvince(lat: number, lng: number): string {
let best = VN_PROVINCE_CENTROIDS[0];
let bestD = Infinity;
for (const p of VN_PROVINCE_CENTROIDS) {
const dLat = lat - p.lat;
const dLng = lng - p.lng;
const d = dLat * dLat + dLng * dLng;
if (d < bestD) {
bestD = d;
best = p;
}
}
return best.name;
}