# app/services/location_validator.py - BAZAGA MOSLASHTIRILGAN (TO'LIQ VERSIYA) import logging import json from typing import Optional, List, Dict from pathlib import Path logger = logging.getLogger(__name__) LOCATIONS_FILE = Path("data/locations_db.json") def load_locations_database() -> Dict: """ locations_db.json faylini yuklash (REAL FORMAT BILAN) Real format: { "regions": [ { "district_name": "Bektemir tumani", "mahallas": [ { "mahalla_name": "Abay mahallasi", "coordinates": {"latitude": 41.2411, "longitude": 69.3455} } ] } ] } Returns: dict: Locations database (simplified format uchun konvert qilingan) """ try: if not LOCATIONS_FILE.exists(): logger.error(f"❌ Locations file topilmadi: {LOCATIONS_FILE}") return {} with open(LOCATIONS_FILE, 'r', encoding='utf-8') as f: data = json.load(f) # ========== REAL FORMATDAN SIMPLIFIED FORMATGA KONVERT ========== simplified = {} regions = data.get('regions', []) for region in regions: districts = region.get('districts', []) for district in districts: district_name = district.get('district_name', '') mahallas_list = district.get('mahallas', []) if not district_name: continue # Mahallalarni dict ga o'girish mahallas_dict = {} for mahalla in mahallas_list: mahalla_name = mahalla.get('mahalla_name', '') coords = mahalla.get('coordinates', {}) if mahalla_name and coords: # latitude/longitude -> lat/lon mahallas_dict[mahalla_name] = { "lat": coords.get('latitude'), "lon": coords.get('longitude') } simplified[district_name] = mahallas_dict logger.info(f"✅ Locations DB yuklandi va konvert qilindi: {len(simplified)} ta tuman") return simplified except Exception as e: logger.error(f"❌ Locations DB yuklashda xatolik: {e}", exc_info=True) return {} def normalize_district_name(district_name: str) -> str: """ Tuman nomini normallashtirish Args: district_name: "Chilonzor tumani" yoki "Chilonzor" Returns: "Chilonzor tumani" (doim "tumani" bilan) """ name = district_name.strip() # "tumani" yoki "tuman" ni olib tashlash name = name.replace(' tumani', '').replace(' tuman', '').strip() # Qaytadan qo'shish return f"{name} tumani" def normalize_mahalla_name(mahalla_name: str) -> str: """ Mahalla nomini normallashtirish Args: mahalla_name: "Katta Chilonzor mahallasi" yoki "Katta Chilonzor" Returns: "Katta Chilonzor mahallasi" (doim "mahallasi" bilan) """ name = mahalla_name.strip() # "mahallasi" yoki "mahalla" ni olib tashlash name = name.replace(' mahallasi', '').replace(' mahalla', '').strip() # Qaytadan qo'shish return f"{name} mahallasi" def get_mahalla_coordinates(district_name: str, mahalla_name: str) -> Optional[Dict]: """ Mahalla koordinatalarini olish (REAL BAZADAN) Args: district_name: "Chilonzor tumani" yoki "Chilonzor" mahalla_name: "Katta Chilonzor mahallasi" yoki "Katta Chilonzor" Returns: {"lat": 41.xxx, "lon": 69.xxx} yoki None """ try: logger.info(f"🔍 Mahalla koordinatalari qidirilmoqda: '{mahalla_name}', '{district_name}'") locations_db = load_locations_database() if not locations_db: logger.warning("⚠️ Locations DB bo'sh!") return None # Normalizatsiya normalized_district = normalize_district_name(district_name) normalized_mahalla = normalize_mahalla_name(mahalla_name) logger.info(f"🔍 Normalized: Tuman='{normalized_district}', Mahalla='{normalized_mahalla}'") # Tumanni topish (case-insensitive) district_data = None for db_district_name, mahallas in locations_db.items(): if db_district_name.lower() == normalized_district.lower(): district_data = mahallas logger.info(f"✅ Tuman topildi DB'da: {db_district_name}") break if not district_data: logger.warning(f"⚠️ Tuman topilmadi DB'da: {normalized_district}") return None # Mahallani topish (case-insensitive) for db_mahalla_name, coords in district_data.items(): if db_mahalla_name.lower() == normalized_mahalla.lower(): logger.info(f"✅ Mahalla koordinatalari topildi (DB): {db_mahalla_name} ({coords['lat']}, {coords['lon']})") return {"lat": coords['lat'], "lon": coords['lon']} logger.warning(f"⚠️ Mahalla topilmadi DB'da: {normalized_mahalla}") return None except Exception as e: logger.error(f"❌ Mahalla koordinatalarini olishda xatolik: {e}", exc_info=True) return None def get_mahallas_by_district(district_name: str) -> List[str]: """ Tuman bo'yicha mahallalar ro'yxatini olish (REAL BAZADAN) Args: district_name: "Chilonzor tumani" yoki "Chilonzor" Returns: ["Katta Chilonzor-1 mahallasi", "Beltepa mahallasi", ...] """ try: locations_db = load_locations_database() if not locations_db: logger.warning(f"⚠️ Locations DB bo'sh!") return [] # Normalizatsiya normalized_district = normalize_district_name(district_name) # Tumanni topish (case-insensitive) for db_district_name, mahallas in locations_db.items(): if db_district_name.lower() == normalized_district.lower(): mahalla_names = list(mahallas.keys()) logger.info(f"✅ {len(mahalla_names)} ta mahalla topildi ({db_district_name})") return mahalla_names logger.warning(f"⚠️ Tuman topilmadi: {normalized_district}") return [] except Exception as e: logger.error(f"❌ Mahallalarni olishda xatolik: {e}") return [] def format_mahallas_list(mahallas: List[str], max_items: int = 5) -> str: """ AI javobi uchun mahallalar ro'yxatini formatlash Args: mahallas: Mahallalar ro'yxati max_items: Ko'rsatiladigan maksimal elementlar soni Returns: "Katta Chilonzor, Beltepa, Beshqozon..." """ if not mahallas: return "" # "mahallasi" so'zini olib tashlash (qisqartirish uchun) cleaned_mahallas = [m.replace(' mahallasi', '').replace(' mahalla', '').strip() for m in mahallas] if len(cleaned_mahallas) > max_items: return ", ".join(cleaned_mahallas[:max_items]) + "..." else: return ", ".join(cleaned_mahallas) def get_all_districts() -> List[str]: """ Barcha tumanlar ro'yxati (REAL BAZADAN) Returns: ["Chilonzor tumani", "Bektemir tumani", ...] """ try: locations_db = load_locations_database() if not locations_db: logger.warning("⚠️ Locations DB bo'sh!") return [] districts = list(locations_db.keys()) logger.info(f"✅ {len(districts)} ta tuman ro'yxati olindi") return districts except Exception as e: logger.error(f"❌ Tumanlar ro'yxatini olishda xatolik: {e}") return [] def search_mahalla_fuzzy(district_name: str, query: str, threshold: float = 0.6) -> Optional[str]: """ Mahallani fuzzy search qilish Args: district_name: Tuman nomi query: Qidiruv so'zi threshold: O'xshashlik darajasi Returns: Mahalla nomi yoki None """ try: from difflib import SequenceMatcher mahallas = get_mahallas_by_district(district_name) if not mahallas: return None query_lower = query.lower().strip() best_match = None best_score = 0.0 for mahalla in mahallas: mahalla_lower = mahalla.lower() # Substring match if query_lower in mahalla_lower or mahalla_lower in query_lower: score = 0.9 else: # SequenceMatcher score = SequenceMatcher(None, query_lower, mahalla_lower).ratio() if score > best_score: best_score = score best_match = mahalla if best_score >= threshold: logger.info(f"✅ Fuzzy match: '{best_match}' (score: {best_score:.2f})") return best_match else: logger.warning(f"⚠️ Fuzzy match topilmadi (best: {best_score:.2f})") return None except Exception as e: logger.error(f"❌ Fuzzy search xatolik: {e}") return None def validate_coordinates(lat: float, lon: float) -> bool: """ Koordinatalarning Toshkent chegarasida ekanligini tekshirish Args: lat: Latitude lon: Longitude Returns: True - Toshkent ichida, False - tashqarida """ TASHKENT_BOUNDS = { "lat_min": 41.20, "lat_max": 41.35, "lon_min": 69.10, "lon_max": 69.35 } 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