36 lines
1.3 KiB
Python
36 lines
1.3 KiB
Python
"""Geo helpers: ZIP geocoding, haversine distance, bounding-box radius filter.
|
|
|
|
Portable across MySQL and SQLite (no spatial extension needed). For large-scale
|
|
deployments, see README for the MySQL POINT + SPATIAL INDEX upgrade.
|
|
"""
|
|
import math
|
|
from app.models.geo import ZipGeo
|
|
|
|
EARTH_MI = 3958.7613 # mean earth radius, miles
|
|
|
|
|
|
def geocode_zip(zip_code):
|
|
"""Return (lat, lng, city, state, metro) or None."""
|
|
row = ZipGeo.query.get((zip_code or "").strip())
|
|
if row is None:
|
|
return None
|
|
return row.lat, row.lng, row.city, row.state, row.metro
|
|
|
|
|
|
def haversine_mi(lat1, lng1, lat2, lng2):
|
|
rlat1, rlat2 = math.radians(lat1), math.radians(lat2)
|
|
dlat = math.radians(lat2 - lat1)
|
|
dlng = math.radians(lng2 - lng1)
|
|
a = (math.sin(dlat / 2) ** 2
|
|
+ math.cos(rlat1) * math.cos(rlat2) * math.sin(dlng / 2) ** 2)
|
|
return EARTH_MI * 2 * math.asin(math.sqrt(a))
|
|
|
|
|
|
def bounding_box(lat, lng, radius_mi):
|
|
"""Lat/lng min/max box enclosing the radius. Cheap indexed prefilter."""
|
|
lat_delta = radius_mi / 69.0
|
|
# guard against poles; cos(lat) shrinks lng degrees-per-mile
|
|
cos_lat = max(math.cos(math.radians(lat)), 0.01)
|
|
lng_delta = radius_mi / (69.172 * cos_lat)
|
|
return (lat - lat_delta, lat + lat_delta, lng - lng_delta, lng + lng_delta)
|