// Pure geographic math — no network, no DB (CLAUDE.md: pure functions, // inject data, no I/O in the math). Geocoding itself (address -> lat/lng) // is I/O and lives in services/zones.server.ts; this file is just the // distance/eligibility arithmetic once coordinates are known. export interface Coordinates { lat: number; lng: number; } const EARTH_RADIUS_KM = 6371; function toRadians(degrees: number): number { return (degrees * Math.PI) / 180; } /** Great-circle (straight-line) distance between two points, in kilometers. */ export function haversineDistanceKm(a: Coordinates, b: Coordinates): number { const dLat = toRadians(b.lat - a.lat); const dLng = toRadians(b.lng - a.lng); const lat1 = toRadians(a.lat); const lat2 = toRadians(b.lat); const h = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) ** 2; const c = 2 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h)); return EARTH_RADIUS_KM * c; } export function isWithinRadiusKm(point: Coordinates, center: Coordinates, radiusKm: number): boolean { return haversineDistanceKm(point, center) <= radiusKm; } /** Loose normalization for postal/ZIP comparison: uppercase, strip spaces. Matches "V6B 1A1" against "v6b1a1". */ export function normalizePostalCode(code: string): string { return code.toUpperCase().replace(/\s+/g, ""); } export function isPostalCodeListed(postalCode: string, listed: string[]): boolean { const normalized = normalizePostalCode(postalCode); return listed.some((entry) => normalizePostalCode(entry) === normalized); } /** Sorts locations by distance from a point, nearest first. */ export function sortByDistance(point: Coordinates, items: T[]): T[] { return [...items].sort( (a, b) => haversineDistanceKm(point, a.coordinates) - haversineDistanceKm(point, b.coordinates), ); }