feat: move Photon geocoder config to config.yaml — public komoot default, local override in deployment config

This commit is contained in:
Matt Johnson (via Claude) 2026-06-10 19:49:26 +00:00
commit 72611cc148
3 changed files with 56 additions and 13 deletions

View file

@ -25,7 +25,7 @@ import urllib.request
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, Optional from typing import Any, Optional
from meshai.adapter_config import adapter_config # Geocoder config is set via init_geocoder_config()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -248,13 +248,31 @@ def _is_uninformative_road(road: Optional[str]) -> bool:
# 2026-06-04). It's the same Echo6-local Photon instance that backs Central's # 2026-06-04). It's the same Echo6-local Photon instance that backs Central's
# NaviBackend reverse-geocoder. Photon takes osm_tag=place (KEY only, not # NaviBackend reverse-geocoder. Photon takes osm_tag=place (KEY only, not
# key:value with comma-list -- that returns 0 features -- per probe). # key:value with comma-list -- that returns 0 features -- per probe).
# v0.6-3b: photon endpoint settings live in adapter_config.geocoder. # v0.6-3b: photon geocoder config - initialized via init_geocoder_config()
# Module-level names retained as backward-compat aliases so existing # Defaults to public Komoot Photon; deployments override in config.yaml.
# test imports / monkeypatches still resolve.
PHOTON_BASE_URL = "http://100.64.0.24:2322" class _GeocoderSettings:
PHOTON_TIMEOUT_S = 2.0 url: str = "https://photon.komoot.io"
PHOTON_RADIUS_KM = 80 # ≈ 50 miles timeout_seconds: float = 2.0
PHOTON_LIMIT = 10 radius_km: float = 80.0
limit: int = 10
_geocoder = _GeocoderSettings()
def init_geocoder_config(url: str = None, timeout: float = None,
radius: float = None, limit: int = None) -> None:
"""Initialize geocoder settings from config.yaml values."""
if url is not None:
_geocoder.url = url
if timeout is not None:
_geocoder.timeout_seconds = timeout
if radius is not None:
_geocoder.radius_km = radius
if limit is not None:
_geocoder.limit = limit
# OSM place classes we accept as "town". Suburb included for metro coverage; # OSM place classes we accept as "town". Suburb included for metro coverage;
# locality is rare but valid for tiny rural places. # locality is rare but valid for tiny rural places.
_TOWN_OSM_VALUES = frozenset({"city", "town", "village"}) _TOWN_OSM_VALUES = frozenset({"city", "town", "village"})
@ -282,13 +300,13 @@ def _photon_reverse_places(lat: float, lon: float) -> list[dict]:
qs = urllib.parse.urlencode({ qs = urllib.parse.urlencode({
"lat": f"{lat:.6f}", "lat": f"{lat:.6f}",
"lon": f"{lon:.6f}", "lon": f"{lon:.6f}",
"radius": PHOTON_RADIUS_KM, "radius": _geocoder.radius_km,
"osm_tag": "place", "osm_tag": "place",
"limit": PHOTON_LIMIT, "limit": _geocoder.limit,
}) })
url = f"{PHOTON_BASE_URL}/reverse?{qs}" url = f"{_geocoder.url}/reverse?{qs}"
try: try:
with urllib.request.urlopen(url, timeout=PHOTON_TIMEOUT_S) as resp: with urllib.request.urlopen(url, timeout=_geocoder.timeout_seconds) as resp:
body = resp.read() body = resp.read()
d = json.loads(body) d = json.loads(body)
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError,
@ -308,7 +326,7 @@ def nearest_town(lat: float, lon: float, max_distance_mi: float = 50.0) -> Optio
event is N of the town. Returns None if no town within range or if event is N of the town. Returns None if no town within range or if
Photon is unreachable. Photon is unreachable.
Calls Photon /reverse?osm_tag=place at PHOTON_BASE_URL. Results are Calls Photon /reverse?osm_tag=place at _geocoder.url. Results are
H3-cell-cached (resolution 7 5 km cells) so the second event near H3-cell-cached (resolution 7 5 km cells) so the second event near
the same town is free. the same town is free.
""" """

View file

@ -468,6 +468,16 @@ class CentralConsumerConfig:
region: str = "us.id" region: str = "us.id"
@dataclass
class GeocoderConfig:
"""Photon reverse geocoder settings."""
url: str = "https://photon.komoot.io"
timeout_seconds: float = 2.0
radius_km: float = 80.0
limit: int = 10
@dataclass @dataclass
class EnvironmentalConfig: class EnvironmentalConfig:
"""Environmental feeds settings.""" """Environmental feeds settings."""
@ -486,6 +496,7 @@ class EnvironmentalConfig:
wzdx: WZDxConfig = field(default_factory=WZDxConfig) wzdx: WZDxConfig = field(default_factory=WZDxConfig)
firms: FIRMSConfig = field(default_factory=FIRMSConfig) firms: FIRMSConfig = field(default_factory=FIRMSConfig)
central: CentralConsumerConfig = field(default_factory=CentralConsumerConfig) central: CentralConsumerConfig = field(default_factory=CentralConsumerConfig)
geocoder: GeocoderConfig = field(default_factory=GeocoderConfig)
@dataclass @dataclass

View file

@ -19,6 +19,7 @@ from .commands.status import set_start_time
from .config import Config from .config import Config
from .config_loader import load_config, get_config_dir_from_path from .config_loader import load_config, get_config_dir_from_path
from .connector import MeshConnector, MeshMessage from .connector import MeshConnector, MeshMessage
from .central_normalizer import init_geocoder_config
from .context import MeshContext from .context import MeshContext
from .history import ConversationHistory from .history import ConversationHistory
from .memory import ConversationSummary from .memory import ConversationSummary
@ -245,6 +246,19 @@ class MeshAI:
except Exception: except Exception:
logger.exception("persistence init_db failed at startup") logger.exception("persistence init_db failed at startup")
# v0.6-3b: Initialize geocoder config from config.yaml
try:
gc = self.config.environmental.geocoder
init_geocoder_config(
url=gc.url,
timeout=gc.timeout_seconds,
radius=gc.radius_km,
limit=gc.limit
)
logger.info("Geocoder configured: %s", gc.url)
except Exception:
logger.exception("geocoder init failed - using defaults")
# Conversation history # Conversation history
self.history = ConversationHistory(self.config.history) self.history = ConversationHistory(self.config.history)
await self.history.initialize() await self.history.initialize()