import { readFileSync, existsSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; /** * Offline point-in-country lookup (Natural Earth 1:50m admin-0, public domain) — used when a source publishes * coordinates but no country (OpenStreetMap features rarely carry addr:country). Deterministic, no network. */ type Ring = number[][]; interface Feat { iso2: string; bbox: [number, number, number, number]; polys: Ring[][] } let feats: Feat[] | null = null; function load(): Feat[] { if (feats) return feats; const here = dirname(fileURLToPath(import.meta.url)); const candidates = [join(here, "..", "..", "data", "ne_50m_admin_0_countries.geojson"), join(process.cwd(), "apps", "worker", "data", "ne_50m_admin_0_countries.geojson"), join(process.cwd(), "data", "ne_50m_admin_0_countries.geojson")]; const file = candidates.find((f) => existsSync(f)); if (!file) { feats = []; return feats; } const gj = JSON.parse(readFileSync(file, "utf8")) as { features: Array<{ properties: Record; geometry: { type: string; coordinates: unknown } }> }; const out: Feat[] = []; for (const f of gj.features) { const p = f.properties; let iso2 = String(p.ISO_A2_EH ?? p.ISO_A2 ?? "-99"); if (iso2 === "-99") { const a = String(p.ADM0_A3 ?? ""); iso2 = ({ FRA: "FR", NOR: "NO", KOS: "XK", SDS: "SS", SOL: "SO" } as Record)[a] ?? "-99"; } if (!/^[A-Z]{2}$/.test(iso2)) continue; const polys: Ring[][] = f.geometry.type === "Polygon" ? [f.geometry.coordinates as Ring[]] : f.geometry.type === "MultiPolygon" ? (f.geometry.coordinates as Ring[][]) : []; let minX = 180, minY = 90, maxX = -180, maxY = -90; for (const poly of polys) for (const [x, y] of poly[0] ?? []) { if (x! < minX) minX = x!; if (x! > maxX) maxX = x!; if (y! < minY) minY = y!; if (y! > maxY) maxY = y!; } out.push({ iso2, bbox: [minX, minY, maxX, maxY], polys }); } feats = out; return feats; } function inRing(x: number, y: number, ring: Ring): boolean { let inside = false; for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) { const xi = ring[i]![0]!, yi = ring[i]![1]!, xj = ring[j]![0]!, yj = ring[j]![1]!; if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) inside = !inside; } return inside; } /** ISO-2 of the country containing the point, or null (ocean / unknown). Handles holes. */ export function countryFromPoint(lat: number, lng: number): string | null { if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null; for (const f of load()) { const [minX, minY, maxX, maxY] = f.bbox; if (lng < minX || lng > maxX || lat < minY || lat > maxY) continue; for (const poly of f.polys) { if (!poly[0] || !inRing(lng, lat, poly[0])) continue; let hole = false; for (let k = 1; k < poly.length; k++) if (inRing(lng, lat, poly[k]!)) { hole = true; break; } if (!hole) return f.iso2; } } return null; }