""" Nominatim Client for geocoding and address search """ import structlog import httpx from typing import Optional, List, Dict, Any from shared.config.base import BaseServiceSettings logger = structlog.get_logger() class NominatimClient: """ Client for Nominatim geocoding service. Provides address search and geocoding capabilities for the bakery onboarding flow. """ def __init__(self, config: BaseServiceSettings): self.config = config self.nominatim_url = getattr( config, "NOMINATIM_SERVICE_URL", "http://nominatim-service:8080" ) self.timeout = 30 async def search_address( self, query: str, country_codes: str = "es", limit: int = 5, addressdetails: bool = True ) -> List[Dict[str, Any]]: """ Search for addresses matching a query. Args: query: Address search query (e.g., "Calle Mayor 1, Madrid") country_codes: Limit search to country codes (default: "es" for Spain) limit: Maximum number of results (default: 5) addressdetails: Include detailed address breakdown (default: True) Returns: List of geocoded results with lat, lon, and address details Example: results = await nominatim.search_address("Calle Mayor 1, Madrid") if results: lat = results[0]["lat"] lon = results[0]["lon"] display_name = results[0]["display_name"] """ try: async with httpx.AsyncClient(timeout=self.timeout) as client: response = await client.get( f"{self.nominatim_url}/search", params={ "q": query, "format": "json", "countrycodes": country_codes, "addressdetails": 1 if addressdetails else 0, "limit": limit } ) if response.status_code == 200: results = response.json() logger.info( "Address search completed", query=query, results_count=len(results) ) return results else: logger.error( "Nominatim search failed", query=query, status_code=response.status_code, response=response.text ) return [] except httpx.TimeoutException: logger.error("Nominatim search timeout", query=query) return [] except Exception as e: logger.error("Nominatim search error", query=query, error=str(e)) return [] async def geocode_address( self, street: str, city: str, postal_code: Optional[str] = None, country: str = "Spain" ) -> Optional[Dict[str, Any]]: """ Geocode a structured address to coordinates. Args: street: Street name and number city: City name postal_code: Optional postal code country: Country name (default: "Spain") Returns: Dict with lat, lon, and display_name, or None if not found Example: location = await nominatim.geocode_address( street="Calle Mayor 1", city="Madrid", postal_code="28013" ) if location: lat, lon = location["lat"], location["lon"] """ # Build structured query query_parts = [street, city] if postal_code: query_parts.append(postal_code) query_parts.append(country) query = ", ".join(query_parts) results = await self.search_address(query, limit=1) if results: return results[0] return None async def reverse_geocode( self, latitude: float, longitude: float ) -> Optional[Dict[str, Any]]: """ Reverse geocode coordinates to an address. Args: latitude: Latitude coordinate longitude: Longitude coordinate Returns: Dict with address details, or None if not found Example: address = await nominatim.reverse_geocode(40.4168, -3.7038) if address: city = address["address"]["city"] street = address["address"]["road"] """ try: async with httpx.AsyncClient(timeout=self.timeout) as client: response = await client.get( f"{self.nominatim_url}/reverse", params={ "lat": latitude, "lon": longitude, "format": "json", "addressdetails": 1 } ) if response.status_code == 200: result = response.json() logger.info( "Reverse geocoding completed", lat=latitude, lon=longitude ) return result else: logger.error( "Nominatim reverse geocoding failed", lat=latitude, lon=longitude, status_code=response.status_code ) return None except Exception as e: logger.error( "Reverse geocoding error", lat=latitude, lon=longitude, error=str(e) ) return None async def health_check(self) -> bool: """ Check if Nominatim service is healthy. Returns: True if service is responding, False otherwise """ try: async with httpx.AsyncClient(timeout=5) as client: response = await client.get(f"{self.nominatim_url}/status") return response.status_code == 200 except Exception as e: logger.warning("Nominatim health check failed", error=str(e)) return False