/** Geo helpers for reconciliation (bounding boxes, precision comparisons). */ import { haversineKm, PRECISION_RANK, validLatLng, type GeoPoint, type GeoPrecision } from "@dci/core"; export interface BBox { minLat: number; maxLat: number; minLng: number; maxLng: number; } /** Bounding box around a point for a radius in km (cheap pre-filter before haversine). */ export function bboxAround(lat: number, lng: number, radiusKm: number): BBox { const dLat = radiusKm / 111.32; const cos = Math.max(0.05, Math.cos((lat * Math.PI) / 180)); const dLng = radiusKm / (111.32 * cos); return { minLat: Math.max(-90, lat - dLat), maxLat: Math.min(90, lat + dLat), minLng: Math.max(-180, lng - dLng), maxLng: Math.min(180, lng + dLng) }; } export function distanceKm(a: { lat: number; lng: number }, b: { lat: number; lng: number }): number { return haversineKm(a.lat, a.lng, b.lat, b.lng); } export function asPrecision(p: string | null | undefined): GeoPrecision { return p && p in PRECISION_RANK ? (p as GeoPrecision) : "unknown"; } /** True when the incoming point may replace the stored one (never degrade precision). */ export function mayReplaceGeo(incoming: GeoPoint | null | undefined, storedPrecision: string | null | undefined, storedHasCoords: boolean): boolean { if (!incoming || !validLatLng(incoming.lat, incoming.lng)) return false; if (!storedHasCoords) return true; return PRECISION_RANK[incoming.precision] >= PRECISION_RANK[asPrecision(storedPrecision)]; } export function validGeo(g: GeoPoint | null | undefined): g is GeoPoint { return !!g && validLatLng(g.lat, g.lng); }