58 lines
1.7 KiB
Python
58 lines
1.7 KiB
Python
"""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))
|
|
|