# app/services/geocoding.py """ Geocoding Service - Manzilni koordinataga aylantirish Nominatim API (OpenStreetMap) ishlatiladi """ import logging from typing import Optional, Dict from geopy.geocoders import Nominatim from geopy.exc import GeocoderTimedOut, GeocoderServiceError import time from app.core.config import NOMINATIM_USER_AGENT, GEOCODING_TIMEOUT, TASHKENT_BOUNDS logger = logging.getLogger(__name__) # Nominatim client (rate limit: 1 request/second) geolocator = Nominatim(user_agent=NOMINATIM_USER_AGENT, timeout=GEOCODING_TIMEOUT) # Rate limiting last_request_time = 0 MIN_REQUEST_INTERVAL = 1.0 # 1 second between requests def _wait_for_rate_limit(): """Rate limit uchun kutish (1 req/sec)""" global last_request_time current_time = time.time() time_since_last = current_time - last_request_time if time_since_last < MIN_REQUEST_INTERVAL: wait_time = MIN_REQUEST_INTERVAL - time_since_last logger.debug(f"⏳ Rate limit: {wait_time:.2f}s kutilmoqda...") time.sleep(wait_time) last_request_time = time.time() def geocode_address(address_text: str) -> Optional[Dict]: """ Manzil matnini koordinataga aylantirish Args: address_text: Manzil matni (masalan: "Chilonzor tumani, Bunyodkor ko'chasi") Returns: { "lat": 41.2856, "lon": 69.2034, "display_name": "To'liq manzil", "address": {...} } yoki None (xatolik bo'lsa) """ if not address_text or len(address_text.strip()) < 3: logger.warning("❌ Bo'sh yoki juda qisqa manzil") return None try: # Toshkent qo'shib qidirish (aniqlik uchun) search_query = f"{address_text}, Toshkent, O'zbekiston" logger.info(f"🔍 Geocoding: '{search_query}'") # Rate limit _wait_for_rate_limit() # Nominatim API call location = geolocator.geocode( search_query, exactly_one=True, language='uz', addressdetails=True ) if not location: logger.warning(f"⚠️ Manzil topilmadi: '{address_text}'") return None # Koordinatalarni tekshirish if not validate_location_in_tashkent(location.latitude, location.longitude): logger.warning(f"⚠️ Koordinatalar Toshkent chegarasidan tashqarida: {location.latitude}, {location.longitude}") return None result = { "lat": location.latitude, "lon": location.longitude, "display_name": location.address, "address": location.raw.get('address', {}) } logger.info(f"✅ Geocoding muvaffaqiyatli: {result['lat']}, {result['lon']}") return result except GeocoderTimedOut: logger.error(f"⏱️ Geocoding timeout: '{address_text}'") return None except GeocoderServiceError as e: logger.error(f"❌ Geocoding service xatoligi: {e}") return None except Exception as e: logger.error(f"❌ Geocoding kutilmagan xatolik: {e}", exc_info=True) return None def reverse_geocode(lat: float, lon: float) -> Optional[Dict]: """ Koordinatalardan manzilni topish (reverse geocoding) Args: lat: Latitude lon: Longitude Returns: { "display_name": "To'liq manzil", "address": { "suburb": "Chilonzor", "city": "Toshkent", ... } } yoki None """ try: # Koordinatalarni validatsiya qilish if not validate_location_in_tashkent(lat, lon): logger.warning(f"⚠️ Koordinatalar Toshkent chegarasidan tashqarida: {lat}, {lon}") return None logger.info(f"🔍 Reverse geocoding: {lat}, {lon}") # Rate limit _wait_for_rate_limit() # Nominatim API call location = geolocator.reverse( (lat, lon), exactly_one=True, language='uz', addressdetails=True ) if not location: logger.warning(f"⚠️ Manzil topilmadi: {lat}, {lon}") return None result = { "display_name": location.address, "address": location.raw.get('address', {}) } logger.info(f"✅ Reverse geocoding muvaffaqiyatli: {result['display_name']}") return result except GeocoderTimedOut: logger.error(f"⏱️ Reverse geocoding timeout: {lat}, {lon}") return None except GeocoderServiceError as e: logger.error(f"❌ Reverse geocoding service xatoligi: {e}") return None except Exception as e: logger.error(f"❌ Reverse geocoding kutilmagan xatolik: {e}", exc_info=True) return None def validate_location_in_tashkent(lat: float, lon: float) -> bool: """ Koordinatalarning Toshkent chegarasida ekanligini tekshirish Args: lat: Latitude lon: Longitude Returns: True - Toshkent ichida, False - tashqarida """ try: in_bounds = ( TASHKENT_BOUNDS["lat_min"] <= lat <= TASHKENT_BOUNDS["lat_max"] and TASHKENT_BOUNDS["lon_min"] <= lon <= TASHKENT_BOUNDS["lon_max"] ) if not in_bounds: logger.warning(f"⚠️ Koordinatalar Toshkent chegarasidan tashqarida: {lat}, {lon}") return in_bounds except Exception as e: logger.error(f"❌ Koordinatalarni tekshirishda xatolik: {e}") return False def get_location_summary(geocoded_data: Dict) -> str: """ Geocoding natijasidan qisqacha manzil chiqarish Args: geocoded_data: geocode_address() natijasi Returns: "Chilonzor tumani, Toshkent" kabi qisqa manzil """ try: address = geocoded_data.get('address', {}) # Tuman/mahalla suburb = address.get('suburb', address.get('neighbourhood', '')) # Ko'cha road = address.get('road', '') # Shahar city = address.get('city', address.get('town', 'Toshkent')) # Qisqacha manzil parts = [] if suburb: parts.append(suburb) if road and suburb.lower() not in road.lower(): # Takrorlanishni oldini olish parts.append(road) if city and city != 'Toshkent': parts.append(city) summary = ', '.join(parts) if parts else geocoded_data.get('display_name', 'Noma\'lum manzil') return summary except Exception as e: logger.error(f"❌ Location summary yaratishda xatolik: {e}") return "Noma'lum manzil" def extract_district_from_address(geocoded_data: Dict) -> Optional[str]: """ Geocoding natijasidan tuman nomini chiqarish Args: geocoded_data: geocode_address() natijasi Returns: "Chilonzor" kabi tuman nomi yoki None """ try: address = geocoded_data.get('address', {}) # Suburb yoki neighbourhood maydonidan tuman nomini olish district = address.get('suburb', address.get('neighbourhood', None)) if district: # "tumani" so'zini olib tashlash district = district.replace(' tumani', '').replace(' Tumani', '').strip() logger.info(f"📍 Tuman aniqlandi: {district}") return district except Exception as e: logger.error(f"❌ Tumanni chiqarishda xatolik: {e}") return None