Add POI feature and imporve the overall backend implementation

This commit is contained in:
Urtzi Alfaro
2025-11-12 15:34:10 +01:00
parent e8096cd979
commit 5783c7ed05
173 changed files with 16862 additions and 9078 deletions

302
services/external/app/api/geocoding.py vendored Normal file
View File

@@ -0,0 +1,302 @@
"""
Geocoding API Endpoints
Provides address search, autocomplete, and geocoding via Nominatim.
"""
from fastapi import APIRouter, Query, HTTPException
from typing import List, Optional
from pydantic import BaseModel, Field
import structlog
from app.services.nominatim_service import NominatimService
logger = structlog.get_logger()
router = APIRouter(prefix="/api/v1/geocoding", tags=["Geocoding"])
# Initialize Nominatim service
# In production, override with environment variable for self-hosted instance
nominatim_service = NominatimService()
# Response Models
class AddressResult(BaseModel):
"""Address search result"""
display_name: str = Field(..., description="Full formatted address")
lat: float = Field(..., description="Latitude")
lon: float = Field(..., description="Longitude")
osm_type: str = Field(..., description="OSM object type")
osm_id: int = Field(..., description="OSM object ID")
place_id: int = Field(..., description="Nominatim place ID")
type: str = Field(..., description="Place type")
class_: str = Field(..., alias="class", description="OSM class")
address: dict = Field(..., description="Parsed address components")
boundingbox: List[str] = Field(..., description="Bounding box coordinates")
class GeocodeResult(BaseModel):
"""Geocoding result"""
display_name: str = Field(..., description="Full formatted address")
lat: float = Field(..., description="Latitude")
lon: float = Field(..., description="Longitude")
address: dict = Field(..., description="Parsed address components")
class CoordinateValidation(BaseModel):
"""Coordinate validation result"""
valid: bool = Field(..., description="Whether coordinates are valid")
address: Optional[str] = Field(None, description="Address at coordinates if valid")
# Endpoints
@router.get(
"/search",
response_model=List[AddressResult],
summary="Search for addresses",
description="Search for addresses matching query (autocomplete). Minimum 3 characters required."
)
async def search_addresses(
q: str = Query(..., min_length=3, description="Search query (minimum 3 characters)"),
country_code: str = Query("es", description="ISO country code to restrict search"),
limit: int = Query(10, ge=1, le=50, description="Maximum number of results")
):
"""
Search for addresses matching the query.
This endpoint provides autocomplete functionality for address input.
Results are restricted to the specified country and sorted by relevance.
Example:
GET /api/v1/geocoding/search?q=Gran%20Via%20Madrid&limit=5
"""
try:
results = await nominatim_service.search_address(
query=q,
country_code=country_code,
limit=limit
)
logger.info(
"Address search request",
query=q,
country=country_code,
result_count=len(results)
)
return results
except Exception as e:
logger.error(
"Address search failed",
query=q,
error=str(e),
exc_info=True
)
raise HTTPException(
status_code=500,
detail=f"Address search failed: {str(e)}"
)
@router.get(
"/geocode",
response_model=GeocodeResult,
summary="Geocode an address",
description="Convert an address string to coordinates (lat/lon)"
)
async def geocode_address(
address: str = Query(..., min_length=5, description="Full address to geocode"),
country_code: str = Query("es", description="ISO country code")
):
"""
Geocode an address to get coordinates.
Returns the best matching location for the given address.
Example:
GET /api/v1/geocoding/geocode?address=Gran%20Via%2028,%20Madrid
"""
try:
result = await nominatim_service.geocode_address(
address=address,
country_code=country_code
)
if not result:
raise HTTPException(
status_code=404,
detail=f"Address not found: {address}"
)
logger.info(
"Geocoding request",
address=address,
lat=result["lat"],
lon=result["lon"]
)
return result
except HTTPException:
raise
except Exception as e:
logger.error(
"Geocoding failed",
address=address,
error=str(e),
exc_info=True
)
raise HTTPException(
status_code=500,
detail=f"Geocoding failed: {str(e)}"
)
@router.get(
"/reverse",
response_model=GeocodeResult,
summary="Reverse geocode coordinates",
description="Convert coordinates (lat/lon) to an address"
)
async def reverse_geocode(
lat: float = Query(..., ge=-90, le=90, description="Latitude"),
lon: float = Query(..., ge=-180, le=180, description="Longitude")
):
"""
Reverse geocode coordinates to get address.
Returns the address at the specified coordinates.
Example:
GET /api/v1/geocoding/reverse?lat=40.4168&lon=-3.7038
"""
try:
result = await nominatim_service.reverse_geocode(
latitude=lat,
longitude=lon
)
if not result:
raise HTTPException(
status_code=404,
detail=f"No address found at coordinates: {lat}, {lon}"
)
logger.info(
"Reverse geocoding request",
lat=lat,
lon=lon,
address=result["display_name"]
)
return result
except HTTPException:
raise
except Exception as e:
logger.error(
"Reverse geocoding failed",
lat=lat,
lon=lon,
error=str(e),
exc_info=True
)
raise HTTPException(
status_code=500,
detail=f"Reverse geocoding failed: {str(e)}"
)
@router.get(
"/validate",
response_model=CoordinateValidation,
summary="Validate coordinates",
description="Check if coordinates point to a valid location"
)
async def validate_coordinates(
lat: float = Query(..., ge=-90, le=90, description="Latitude"),
lon: float = Query(..., ge=-180, le=180, description="Longitude")
):
"""
Validate that coordinates point to a real location.
Returns validation result with address if valid.
Example:
GET /api/v1/geocoding/validate?lat=40.4168&lon=-3.7038
"""
try:
is_valid = await nominatim_service.validate_coordinates(
latitude=lat,
longitude=lon
)
result = {"valid": is_valid, "address": None}
if is_valid:
geocode_result = await nominatim_service.reverse_geocode(lat, lon)
if geocode_result:
result["address"] = geocode_result["display_name"]
logger.info(
"Coordinate validation request",
lat=lat,
lon=lon,
valid=is_valid
)
return result
except Exception as e:
logger.error(
"Coordinate validation failed",
lat=lat,
lon=lon,
error=str(e),
exc_info=True
)
raise HTTPException(
status_code=500,
detail=f"Coordinate validation failed: {str(e)}"
)
@router.get(
"/health",
summary="Check geocoding service health",
description="Check if Nominatim service is accessible"
)
async def health_check():
"""
Check if Nominatim service is accessible.
Returns service health status.
"""
try:
is_healthy = await nominatim_service.health_check()
if not is_healthy:
raise HTTPException(
status_code=503,
detail="Nominatim service is unavailable"
)
return {
"status": "healthy",
"service": "nominatim",
"base_url": nominatim_service.base_url,
"is_public_api": nominatim_service.is_public_api
}
except HTTPException:
raise
except Exception as e:
logger.error(
"Health check failed",
error=str(e),
exc_info=True
)
raise HTTPException(
status_code=503,
detail=f"Health check failed: {str(e)}"
)