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
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>
93 lines
3.6 KiB
TypeScript
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;
|
|
}
|