"""OpenStreetMap Overpass API クライアント""" import time from typing import List, Optional, Dict, Any from dataclasses import dataclass from cachetools import TTLCache from config.settings import settings @dataclass class POI: """Point of Interest""" osm_id: int name: str lat: float lon: float poi_type: str tags: Dict[str, str] class OverpassClient: """ OpenStreetMap Overpass API クライアント POI検索とキャッシュ機能を提供 """ OVERPASS_URL = "https://overpass-api.de/api/interpreter" # 店舗タイプマッピング SHOP_TYPE_MAPPING = { "convenience_store": ["shop=convenience", "amenity=convenience"], "restaurant": ["amenity=restaurant", "amenity=fast_food"], "hospital": ["amenity=hospital", "amenity=clinic"], "pharmacy": ["amenity=pharmacy", "shop=chemist"], "gas_station": ["amenity=fuel"], "parking": ["amenity=parking"], "station": ["railway=station", "public_transport=station"], "bank": ["amenity=bank"], "post_office": ["amenity=post_office"], "supermarket": ["shop=supermarket"], } def __init__(self, timeout: int = 25, cache_ttl: int = 300): self.timeout = timeout self._cache = TTLCache(maxsize=100, ttl=cache_ttl) self._last_request_time: float = 0 self._min_interval: float = 1.0 # 最低1秒間隔 def _wait_for_rate_limit(self) -> None: """レート制限のための待機""" elapsed = time.time() - self._last_request_time if elapsed < self._min_interval: time.sleep(self._min_interval - elapsed) def _build_name_query( self, name: str, lat: float, lon: float, radius: int, ) -> str: """名前でPOIを検索するクエリを構築""" return f""" [out:json][timeout:{self.timeout}]; ( node["name"~"{name}",i](around:{radius},{lat},{lon}); way["name"~"{name}",i](around:{radius},{lat},{lon}); ); out center; """ def _build_type_query( self, poi_type: str, lat: float, lon: float, radius: int, ) -> str: """タイプでPOIを検索するクエリを構築""" tags = self.SHOP_TYPE_MAPPING.get(poi_type, []) if not tags: return "" conditions = [] for tag in tags: key, value = tag.split("=") conditions.append(f'node["{key}"="{value}"](around:{radius},{lat},{lon});') conditions.append(f'way["{key}"="{value}"](around:{radius},{lat},{lon});') return f""" [out:json][timeout:{self.timeout}]; ( {chr(10).join(conditions)} ); out center; """ def _parse_response(self, data: Dict[str, Any]) -> List[POI]: """Overpass APIレスポンスをパース""" pois = [] elements = data.get("elements", []) for elem in elements: tags = elem.get("tags", {}) name = tags.get("name", "") # 座標の取得(wayの場合はcenter) if elem.get("type") == "way": center = elem.get("center", {}) lat = center.get("lat", 0) lon = center.get("lon", 0) else: lat = elem.get("lat", 0) lon = elem.get("lon", 0) # POIタイプの判定 poi_type = "unknown" if tags.get("shop"): poi_type = tags.get("shop") elif tags.get("amenity"): poi_type = tags.get("amenity") elif tags.get("railway"): poi_type = "station" if lat and lon: pois.append( POI( osm_id=elem.get("id", 0), name=name, lat=lat, lon=lon, poi_type=poi_type, tags=tags, ) ) return pois def search_by_name( self, name: str, lat: float, lon: float, radius: int = 500, ) -> List[POI]: """ 名前でPOIを検索 Args: name: 検索名 lat: 緯度 lon: 経度 radius: 検索半径(メートル) Returns: POIのリスト """ cache_key = f"name:{name}:{lat:.4f}:{lon:.4f}:{radius}" if cache_key in self._cache: return self._cache[cache_key] query = self._build_name_query(name, lat, lon, radius) result = self._execute_query(query) self._cache[cache_key] = result return result def search_by_type( self, poi_type: str, lat: float, lon: float, radius: int = 500, ) -> List[POI]: """ タイプでPOIを検索 Args: poi_type: POIタイプ lat: 緯度 lon: 経度 radius: 検索半径(メートル) Returns: POIのリスト """ cache_key = f"type:{poi_type}:{lat:.4f}:{lon:.4f}:{radius}" if cache_key in self._cache: return self._cache[cache_key] query = self._build_type_query(poi_type, lat, lon, radius) if not query: return [] result = self._execute_query(query) self._cache[cache_key] = result return result def _execute_query(self, query: str) -> List[POI]: """Overpass APIクエリを実行""" try: import requests self._wait_for_rate_limit() self._last_request_time = time.time() response = requests.post( self.OVERPASS_URL, data={"data": query}, timeout=self.timeout, ) response.raise_for_status() return self._parse_response(response.json()) except Exception as e: print(f"Overpass query error: {e}") return [] def search_combined( self, names: List[str], types: List[str], lat: float, lon: float, radius: int = 500, ) -> Dict[str, List[POI]]: """ 複合検索(名前とタイプ両方) Returns: {"names": {...}, "types": {...}} の形式 """ result = {"names": {}, "types": {}} for name in names: pois = self.search_by_name(name, lat, lon, radius) if pois: result["names"][name] = pois for poi_type in types: pois = self.search_by_type(poi_type, lat, lon, radius) if pois: result["types"][poi_type] = pois return result def clear_cache(self) -> None: """キャッシュをクリア""" self._cache.clear()