import type { GeoPrecision } from "./types.js"; const R = 6371.0088; export function haversineKm(lat1: number, lng1: number, lat2: number, lng2: number): number { const toRad = (d: number) => (d * Math.PI) / 180; const dLat = toRad(lat2 - lat1); const dLng = toRad(lng2 - lng1); const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2; return 2 * R * Math.asin(Math.sqrt(a)); } export function validLatLng(lat: unknown, lng: unknown): lat is number { return typeof lat === "number" && typeof lng === "number" && Number.isFinite(lat) && Number.isFinite(lng) && Math.abs(lat) <= 90 && Math.abs(lng) <= 180 && !(lat === 0 && lng === 0); } /** Radius (km) within which two points may be the same facility, depending on the worse precision of the pair. */ export function matchRadiusKm(p: GeoPrecision, q: GeoPrecision): number { const rank: Record = { exact: 0.25, parcel: 0.5, street: 1, city: 15, metro: 40, approximate: 25, unknown: 25 }; return Math.max(rank[p], rank[q]); } export const PRECISION_RANK: Record = { exact: 6, parcel: 5, street: 4, approximate: 3, city: 2, metro: 1, unknown: 0 }; /** Simple geohash (base32) for bucketing / cluster ids. */ export function geohash(lat: number, lng: number, precision = 6): string { const BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz"; let idx = 0, bit = 0, even = true, hash = ""; let latMin = -90, latMax = 90, lngMin = -180, lngMax = 180; while (hash.length < precision) { if (even) { const mid = (lngMin + lngMax) / 2; if (lng >= mid) { idx = idx * 2 + 1; lngMin = mid; } else { idx *= 2; lngMax = mid; } } else { const mid = (latMin + latMax) / 2; if (lat >= mid) { idx = idx * 2 + 1; latMin = mid; } else { idx *= 2; latMax = mid; } } even = !even; if (++bit === 5) { hash += BASE32[idx]; bit = 0; idx = 0; } } return hash; } /** Grid cell for map aggregation at a given zoom (degrees per cell shrinks with zoom). */ export function gridCell(lat: number, lng: number, zoom: number): { key: string; lat: number; lng: number } { const size = 360 / Math.pow(2, Math.max(0, Math.min(zoom, 12)) + 1); const i = Math.floor((lng + 180) / size); const j = Math.floor((lat + 90) / size); return { key: `${zoom}:${i}:${j}`, lat: j * size - 90 + size / 2, lng: i * size - 180 + size / 2 }; }