206 lines
6.4 KiB
Python
Executable File
206 lines
6.4 KiB
Python
Executable File
"""
|
|
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
|