SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
3 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
2.4 KB · 50 lines typescript
Raw Blame History
1import type { GeoPrecision } from "./types.js";23const R = 6371.0088;4export function haversineKm(lat1: number, lng1: number, lat2: number, lng2: number): number {5  const toRad = (d: number) => (d * Math.PI) / 180;6  const dLat = toRad(lat2 - lat1);7  const dLng = toRad(lng2 - lng1);8  const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2;9  return 2 * R * Math.asin(Math.sqrt(a));10}1112export function validLatLng(lat: unknown, lng: unknown): lat is number {13  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);14}1516/** Radius (km) within which two points may be the same facility, depending on the worse precision of the pair. */17export function matchRadiusKm(p: GeoPrecision, q: GeoPrecision): number {18  const rank: Record<GeoPrecision, number> = { exact: 0.25, parcel: 0.5, street: 1, city: 15, metro: 40, approximate: 25, unknown: 25 };19  return Math.max(rank[p], rank[q]);20}2122export const PRECISION_RANK: Record<GeoPrecision, number> = { exact: 6, parcel: 5, street: 4, approximate: 3, city: 2, metro: 1, unknown: 0 };2324/** Simple geohash (base32) for bucketing / cluster ids. */25export function geohash(lat: number, lng: number, precision = 6): string {26  const BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz";27  let idx = 0, bit = 0, even = true, hash = "";28  let latMin = -90, latMax = 90, lngMin = -180, lngMax = 180;29  while (hash.length < precision) {30    if (even) {31      const mid = (lngMin + lngMax) / 2;32      if (lng >= mid) { idx = idx * 2 + 1; lngMin = mid; } else { idx *= 2; lngMax = mid; }33    } else {34      const mid = (latMin + latMax) / 2;35      if (lat >= mid) { idx = idx * 2 + 1; latMin = mid; } else { idx *= 2; latMax = mid; }36    }37    even = !even;38    if (++bit === 5) { hash += BASE32[idx]; bit = 0; idx = 0; }39  }40  return hash;41}4243/** Grid cell for map aggregation at a given zoom (degrees per cell shrinks with zoom). */44export function gridCell(lat: number, lng: number, zoom: number): { key: string; lat: number; lng: number } {45  const size = 360 / Math.pow(2, Math.max(0, Math.min(zoom, 12)) + 1);46  const i = Math.floor((lng + 180) / size);47  const j = Math.floor((lat + 90) / size);48  return { key: `${zoom}:${i}:${j}`, lat: j * size - 90 + size / 2, lng: i * size - 180 + size / 2 };49}50