Files
goodgo-platform/scripts/data/vn-country-polygon.ts
Ho Ngoc Hai d6ac7c316f
Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 8s
CI / E2E Tests (push) Has been skipped
CI / AI Services (Python) — Smoke (push) Failing after 7s
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 1m8s
Deploy / Build API Image (push) Failing after 7s
Deploy / Build Web Image (push) Failing after 6s
Deploy / Build AI Services Image (push) Failing after 5s
E2E Tests / Playwright E2E (push) Failing after 9s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 3s
Security Scanning / Trivy Scan — API Image (push) Failing after 40s
Security Scanning / Trivy Scan — Web Image (push) Failing after 44s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 45s
Security Scanning / Trivy Filesystem Scan (push) Failing after 1m8s
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
feat(industrial): drop non-VN OSM rows + gate sync with country polygon
The OSM bbox sync was picking up `landuse=industrial` polygons that sit
just across the borders in Laos, Thailand, Cambodia and southern China.
After the bulk promote we ended up with 220 of those in the public
catalog — Vientiane SEZ, Phnom Penh SEZ, Sihanoukville SEZ, several
Thai industrial estates etc.

Two-part fix:

1. `scripts/data/vn-country-polygon.ts` — a hand-traced ~30-vertex
   GeoJSON polygon that follows VN's land + sea border. The eastern
   edge is generous (110°E) so every coastal industrial zone (Vũng Áng
   / Formosa, Dung Quất, Nhơn Hội, Vũng Tàu / Long Sơn) sits comfortably
   inside; the western/northern edges trace the actual neighbour
   borders. Includes a pure-JS `isPointInVietnam(lng, lat)` ray-cast
   helper for the sync script (no extra dep).

2. `scripts/prune-non-vietnam-osm.ts` — one-shot cleaner. Uses PostGIS
   `ST_Within(location, polygon)` to delete every OSM row whose centroid
   falls outside. Verified the polygon doesn't reject genuine VN parks
   (Formosa Hà Tĩnh, Dung Quất, Nhơn Hội, KCN Đất Đỏ etc. all pass).

3. `sync-osm-industrial-parks.ts` `parseFeature()` now calls
   `isPointInVietnam` after computing the centroid and bails early on a
   miss, so the next monthly cron run won't re-import them.

Run on dev: removed 220 rows. Final catalog 1,483 KCN, all inside the
Vietnam mainland polygon.

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

93 lines
3.6 KiB
TypeScript

/**
* Simplified Vietnam mainland polygon for "is this point in VN?" tests.
*
* The country has a very long, narrow shape (>1500 km north-south) with
* neighbours pressing in on the western and northern borders. Bbox-based
* filtering (the chunks the Overpass sync uses) inevitably catches some
* `landuse=industrial` polygons that sit just across the border in Laos,
* Cambodia, Thailand and southern China.
*
* The polygon below is a hand-traced ~30-vertex outline that follows the
* official border closely enough for a point-in-polygon test. It's not
* survey-grade — coastal islands are clipped, and the Mekong delta tip
* is rounded — but it's sufficient to reject industrial sites that are
* clearly not in VN. Where edge cases exist (a row landing in the
* 1-2 km buffer near a border crossing), admin can promote / unlock by
* hand from the OSM review queue.
*
* Format: GeoJSON Polygon, coordinates as `[lng, lat]` pairs (per the
* spec). The ring is closed (first === last).
*/
import type { Polygon } from 'geojson';
export const VN_COUNTRY_POLYGON: Polygon = {
type: 'Polygon',
coordinates: [
[
// Northern border, west → east. The northern edge is the actual
// China border line; we trace it loosely.
[102.14, 22.47], // Lai Châu / China junction
[103.0, 22.78], // Lào Cai
[104.0, 22.82], // northern Hà Giang
[105.32, 23.39], // Đồng Văn (northernmost point)
[106.55, 22.95], // Cao Bằng
[107.0, 22.34], // Lạng Sơn
[108.05, 21.55], // Móng Cái / Quảng Ninh
// Eastern boundary at 110°E — generous on the sea side so that
// every coastal industrial zone (Vũng Áng / Formosa, Dung Quất,
// Nhơn Hội, Vũng Tàu, Long Sơn etc.) sits inside. This omits the
// Hoàng Sa / Trường Sa archipelagos — fine, they have no KCN.
[110.0, 21.0],
[110.0, 18.0],
[110.0, 15.0],
[110.0, 12.0],
[110.0, 9.5],
// Mekong delta — Cà Mau cape, then Hà Tiên (south-west tip).
[105.5, 8.4], // south of Cà Mau
[104.83, 8.59], // Cà Mau
[104.45, 10.39], // Hà Tiên
// West / south-west, climbing along the Cambodia + Laos borders.
[105.0, 10.78], // Châu Đốc
[105.85, 11.38], // Tây Ninh
[106.0, 11.7],
[106.6, 11.95], // Lộc Ninh
[107.55, 12.36], // Bù Đăng
[107.55, 14.42], // Kon Tum
[107.32, 16.0], // A Lưới
[106.5, 16.45], // Hướng Hóa
[105.97, 17.69], // Quảng Bình border
[105.18, 18.66], // Hà Tĩnh / Laos border
[104.34, 19.7], // Nghệ An / Laos
[103.95, 20.66], // Mai Châu
[103.05, 21.13], // Sơn La / Laos
[102.78, 21.91], // Điện Biên
[102.14, 22.47], // close ring
],
],
};
/** GeoJSON string ready to feed to PostGIS `ST_GeomFromGeoJSON`. */
export const VN_COUNTRY_POLYGON_GEOJSON = JSON.stringify(VN_COUNTRY_POLYGON);
/**
* Pure-JS point-in-polygon test using the standard ray-casting algorithm.
* Avoids pulling in `@turf/boolean-point-in-polygon` for the sync script
* (one fewer dep, and we only have one polygon to test against).
*/
export function isPointInVietnam(lng: number, lat: number): boolean {
const ring = VN_COUNTRY_POLYGON.coordinates[0];
let inside = false;
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
const xi = ring[i][0];
const yi = ring[i][1];
const xj = ring[j][0];
const yj = ring[j][1];
const intersect =
yi > lat !== yj > lat &&
lng < ((xj - xi) * (lat - yi)) / (yj - yi) + xi;
if (intersect) inside = !inside;
}
return inside;
}