"""Spatial helper functions for H3 recall and distance filtering. These helpers are intentionally small and framework-free so they can be used by agents, API routes, tests, and offline benchmarks. """ from __future__ import annotations import math from dataclasses import dataclass import h3 @dataclass(frozen=True) class Point: lng: float lat: float def haversine_m(a: Point, b: Point) -> float: """Return approximate great-circle distance in meters. Production radius filtering should use PostGIS `geography` + `ST_DWithin`. This function is a fallback for tests or local benchmarks. """ radius = 6_371_008.8 d_lng = math.radians(b.lng - a.lng) d_lat = math.radians(b.lat - a.lat) part = ( math.sin(d_lat / 2) ** 2 + math.cos(math.radians(a.lat)) * math.cos(math.radians(b.lat)) * math.sin(d_lng / 2) ** 2 ) return 2 * radius * math.asin(math.sqrt(part)) def h3_cell(point: Point, resolution: int = 9) -> str: return h3.latlng_to_cell(point.lat, point.lng, resolution) def h3_k_for_radius(radius_m: float, resolution: int = 9, safety_ring: int = 2) -> int: """Compute a conservative H3 grid_disk k for a radius search. H3 recall should be slightly larger than the user's circle so boundary points are not missed. PostGIS does the final exact circle filter. """ edge_m = h3.average_hexagon_edge_length(resolution, unit="m") center_gap_m = math.sqrt(3) * edge_m return max(1, math.ceil(radius_m / center_gap_m) + safety_ring) def h3_neighbor_cells(point: Point, radius_m: float, resolution: int = 9) -> set[str]: center = h3_cell(point, resolution) k = h3_k_for_radius(radius_m, resolution) return set(h3.grid_disk(center, k))