Files
bxh/app/kg_core/spatial.py

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))