"""POI (Points of Interest) API endpoint.""" from pathlib import Path from fastapi import APIRouter, Query import polars as pl router = APIRouter() DATA_FILE = Path("data_sources/uk_pois.parquet") # Category groups with emoji and member categories POI_CATEGORY_GROUPS: dict[str, dict] = { "schools": { "emoji": "🏫", "label": "Schools", "categories": ["school", "preschool", "college_university", "library"], }, "healthcare": { "emoji": "🏥", "label": "Healthcare", "categories": [ "doctor", "dentist", "pharmacy", "hospital", "public_health_clinic", "veterinary", "nursing_home", "social_facility", ], }, "transport": { "emoji": "🚉", "label": "Transport", "categories": [ "train_station", "bus_station", "bus_stop", "metro_station", "light_rail_station", "tram_stop", "ferry_terminal", "airport", ], }, "parks": { "emoji": "🌳", "label": "Parks & Leisure", "categories": [ "park", "national_park", "nature_reserve", "dog_park", "playground", "garden", "sports_centre", "swimming_pool", "gym", "golf_course", "marina", ], }, "emergency": { "emoji": "🚨", "label": "Emergency", "categories": ["police_department", "fire_department"], }, "supermarkets": { "emoji": "🛒", "label": "Supermarkets & Grocery", "categories": [ "supermarket", "grocery_store", "convenience_store", "bakery", "butcher", "greengrocer", "deli", ], }, "shopping": { "emoji": "🛍️", "label": "Shopping", "categories": [ "department_store", "clothing_store", "shoe_store", "electronics_store", "hardware_store", "furniture_store", "bookshop", "newsagent", "charity_shop", "shopping_centre", "optician", "off_licence", ], }, "food_drink": { "emoji": "🍽️", "label": "Food & Drink", "categories": [ "restaurant", "cafe", "pub", "bar", "fast_food", "food_court", "ice_cream", "beer_garden", ], }, "personal_care": { "emoji": "💇", "label": "Personal Care", "categories": [ "hairdresser", "beauty_salon", "laundry", "dry_cleaning", ], }, "finance": { "emoji": "🏦", "label": "Finance", "categories": ["bank", "atm", "bureau_de_change"], }, "entertainment": { "emoji": "🎭", "label": "Entertainment & Culture", "categories": [ "cinema", "theatre", "nightclub", "community_centre", "arts_centre", "museum", "gallery", "attraction", "zoo", "theme_park", "viewpoint", ], }, "accommodation": { "emoji": "🏨", "label": "Accommodation", "categories": [ "hotel", "hostel", "guest_house", "campsite", "caravan_site", ], }, "religion": { "emoji": "🛐", "label": "Places of Worship", "categories": ["place_of_worship"], }, "government": { "emoji": "🏛️", "label": "Government & Public", "categories": [ "town_hall", "courthouse", "post_office", "prison", "public_toilets", ], }, "automotive": { "emoji": "⛽", "label": "Automotive", "categories": [ "petrol_station", "ev_charging", "car_dealer", "car_repair", "parking", "bicycle_parking", ], }, "recycling": { "emoji": "♻️", "label": "Recycling & Waste", "categories": ["recycling", "waste_disposal"], }, } # Flatten for quick lookup ALL_CATEGORIES = { cat for group in POI_CATEGORY_GROUPS.values() for cat in group["categories"] } # Cache the dataframe _df_cache: pl.DataFrame | None = None def get_df() -> pl.DataFrame | None: """Load and cache the POI dataframe.""" global _df_cache if _df_cache is None: if not DATA_FILE.exists(): return None df = pl.read_parquet(DATA_FILE) _df_cache = df.select("id", "name", "category", "lat", "lng").filter( pl.col("category").is_in(ALL_CATEGORIES) ) return _df_cache def preload_pois() -> None: """Preload POI data on startup.""" df = get_df() if df is not None: print(f"Loaded {len(df):,} POIs") @router.get("/pois") async def get_pois( categories: str = Query(..., description="Comma-separated category groups"), bounds: str = Query(..., description="Bounding box: south,west,north,east"), ) -> dict: """Get POIs within bounds for specified category groups.""" df = get_df() if df is None: return {"features": []} try: south, west, north, east = map(float, bounds.split(",")) except ValueError: return {"features": []} requested_groups = [g.strip() for g in categories.split(",")] cats_to_include = set() for group in requested_groups: if group in POI_CATEGORY_GROUPS: cats_to_include.update(POI_CATEGORY_GROUPS[group]["categories"]) if not cats_to_include: return {"features": []} filtered = df.filter( (pl.col("lat") >= south) & (pl.col("lat") <= north) & (pl.col("lng") >= west) & (pl.col("lng") <= east) & (pl.col("category").is_in(cats_to_include)) ) MAX_POIS = 5000 if len(filtered) > MAX_POIS: filtered = filtered.sample(n=MAX_POIS, seed=42) return {"features": filtered.to_dicts()} @router.get("/poi-categories") async def get_poi_categories() -> dict: """Get available POI category groups with emoji and labels.""" return { "categories": { key: {"emoji": group["emoji"], "label": group["label"]} for key, group in POI_CATEGORY_GROUPS.items() } }