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