Spaces:
Paused
Paused
| # 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 |