Minjin1050/mn-geocoding-api
0
1import os2import faiss3import joblib4import csv5import io6import json7import uvicorn8import numpy as np9from fastapi import FastAPI, UploadFile, File, HTTPException10from fastapi.responses import FileResponse11from fastapi.middleware.cors import CORSMiddleware12from sentence_transformers import SentenceTransformer13from pydantic import BaseModel14from typing import Optional, List15from contextlib import asynccontextmanager16from math import radians, cos, sin, asin, sqrt17def haversine_distance(lat1, lon1, lat2, lon2):18 try:19 # Утгуудыг заавал float төрөлд шилжүүлэх (Алдаанаас сэргийлнэ)20 lat1, lon1, lat2, lon2 = map(float, [lat1, lon1, lat2, lon2])21 22 # Хэрэв координатууд ижил бол зай 0 байна23 if lat1 == lat2 and lon1 == lon2:24 return 0.025 26 R = 6371000.0 # Дэлхийн радиус метрээр27 28 # Радиан руу шилжүүлэх29 phi1 = radians(lat1)30 phi2 = radians(lat2)31 d_phi = radians(lat2 - lat1)32 d_lambda = radians(lon2 - lon1)33 34 # Haversine томьёо35 a = sin(d_phi / 2)**2 + cos(phi1) * cos(phi2) * sin(d_lambda / 2)**236 c = 2 * asin(sqrt(a))37 38 distance = R * c39 return distance40 except Exception as e:41 print(f"Зай тооцоолоход алдаа гарлаа: {e}")42 return 0.043DISTRICT_MAPPING = {44 'baynzurkh duureg': 'bayanzurkh duureg', 'baynzurh duureg': 'bayanzurkh duureg',45 'bayanzurh': 'bayanzurkh duureg', 'baynzurh': 'bayanzurkh duureg', 'bzd': 'bayanzurkh duureg',46 'sukhbaatar duureg': 'sukhbaatar duureg', 'sbd': 'sukhbaatar duureg', 'sukhbaatr': 'sukhbaatar duureg',47 'bayangol duureg': 'bayangol duureg', 'bayngol duureg': 'bayangol duureg', 'bgd': 'bayangol duureg',48 'khan uul duureg': 'khan-uul duureg', 'khanuul duureg': 'khan-uul duureg', 'hud': 'khan-uul duureg',49 'han uul duureg': 'khan-uul duureg', 'hanuul duureg': 'khan-uul duureg', 'khud': 'khan-uul duureg',50 'chingeltei duureg': 'chingeltei duureg', 'chingeltii duureg': 'chingeltei duureg', 'chd': 'chingeltei duureg',51 'songinokhairkhan duureg': 'songinokhairkhan duureg', 'songinohairhan duureg': 'songinokhairkhan duureg',52 'shd': 'songinokhairkhan duureg', 'skhd': 'songinokhairkhan duureg'53}54 55CYRILLIC_TO_LATIN_MAP = {56 'А':'a','а':'a','Б':'b','б':'b','В':'v','в':'v','Г':'g','г':'g','Д':'d','д':'d','Е':'e','е':'e','Ё':'yo','ё':'yo',57 'Ж':'j','ж':'j','З':'z','з':'z','И':'i','и':'i','Й':'i','й':'i','К':'k','к':'k','Л':'l','л':'l','М':'m','м':'m',58 'Н':'n','н':'n','О':'o','о':'o','Ө':'o','ө':'o','П':'p','п':'p','Р':'r','р':'r','С':'s','с':'s','Т':'t','т':'t',59 'У':'u','у':'u','Ү':'u','ү':'u','Ф':'f','ф':'f','Х':'kh','х':'kh','Ц':'ts','ц':'ts','Ч':'ch','ч':'ch','Ш':'sh','ш':'sh',60 'Щ':'shch','щ':'shch','Ъ':'','ъ':'','Ы':'ii','ы':'ii','Ь':'','ь':'','Э':'e','э':'e','Ю':'yu','ю':'yu','Я':'ya','я':'ya'61}62 63def preprocess_address(address: str) -> str:64 latin_address = "".join(CYRILLIC_TO_LATIN_MAP.get(c, c) for c in address)65 address_lower = latin_address.lower()66 words = address_lower.split()67 processed_words = [DISTRICT_MAPPING.get(w, w) for w in words]68 final_address = " ".join(processed_words)69 for key, value in DISTRICT_MAPPING.items():70 if key in final_address:71 final_address = final_address.replace(key, value)72 return " ".join(final_address.split())73 74# =======================================================75# 2. ТОХИРГОО БА LIFESPAN76# =======================================================77BASE_DIR = os.path.dirname(os.path.abspath(__file__))78MODEL_PATH = os.path.join(BASE_DIR, "models", "address.pkl")79INDEX_PATH = os.path.join(BASE_DIR, "models", "faiss_final.bin")80FEEDBACK_FILE = "incorrect_predictions.csv"81 82geocoding_model = None83faiss_index = None84embed_model = None85 86@asynccontextmanager87async def lifespan(app: FastAPI):88 global geocoding_model, faiss_index, embed_model89 print("⏳ Hugging Face дээр моделуудыг ачаалж байна...")90 try:91 if os.path.exists(MODEL_PATH) and os.path.exists(INDEX_PATH):92 geocoding_model = joblib.load(MODEL_PATH)93 faiss_index = faiss.read_index(INDEX_PATH)94 embed_model = SentenceTransformer('paraphrase-multilingual-mpnet-base-v2')95 print("✅ Бүх модел амжилттай ачаалагдлаа!")96 else:97 print("❌ Файл олдсонгүй!")98 except Exception as e:99 print(f"❌ Ачаалахад алдаа гарлаа: {e}")100 yield101 102app = FastAPI(lifespan=lifespan)103 104app.add_middleware(105 CORSMiddleware,106 allow_origins=["*"],107 allow_credentials=True,108 allow_methods=["*"],109 allow_headers=["*"],110)111 112# =======================================================113# 3. MODELS & CORE SEARCH LOGIC114# =======================================================115class AddressRequest(BaseModel):116 address: str117 118class ValidationRequest(BaseModel):119 address: str120 user_lat: float121 user_lon: float122 123class AddressResponse(BaseModel):124 input_address: Optional[str] = None125 address: str126 latitude: float127 longitude: float128 match_quality: float129 130def get_coordinates(address: str) -> AddressResponse:131 if embed_model is None:132 return AddressResponse(address="Model not loaded", latitude=0, longitude=0, match_quality=0)133 134 query_vector = embed_model.encode(address, convert_to_numpy=True).astype('float32').reshape(1, -1)135 D, I = faiss_index.search(query_vector, 1)136 idx, dist = I[0][0], D[0][0]137 138 if idx == -1 or dist > 0.8:139 return AddressResponse(address="Олдсонгүй", latitude=0, longitude=0, match_quality=0)140 141 matched_data = geocoding_model.iloc[idx]142 return AddressResponse(143 address=str(matched_data['transliterated_address']),144 latitude=float(matched_data['latitude']),145 longitude=float(matched_data['longitude']),146 match_quality=float(1.0 - dist)147 )148 149# =======================================================150# 4. ENDPOINTS151# =======================================================152 153@app.get("/")154async def serve_frontend():155 return FileResponse("index.html")156 157@app.post("/geocode/single", response_model=AddressResponse)158async def geocode_single(request: AddressRequest):159 processed = preprocess_address(request.address)160 res = get_coordinates(processed)161 res.input_address = request.address162 return res163 164@app.post("/geocode/validate")165async def validate_address(request: ValidationRequest):166 """2-р табад зориулсан: Хаягаар хайж, гараас өгсөн координаттай тулгах"""167 processed = preprocess_address(request.address)168 res = get_coordinates(processed)169 170 if res.address == "Олдсонгүй":171 return {"status": "error", "message": "Модел хаягийг олсонгүй"}172 173 # Зайг тооцоолох174 distance = haversine_distance(request.user_lat, request.user_lon, res.latitude, res.longitude)175 176 return {177 "input_address": request.address,178 "user_coords": {"lat": request.user_lat, "lon": request.user_lon},179 "predicted_coords": {"lat": res.latitude, "lon": res.longitude},180 "predicted_address": res.address,181 "distance_meters": round(distance, 2),182 "is_correct": distance <= 50183 }184 185@app.post("/geocode/batch-file")186async def geocode_batch_file(file: UploadFile = File(...)):187 contents = await file.read()188 addresses = []189 if file.filename.endswith('.csv'):190 reader = csv.DictReader(io.StringIO(contents.decode('utf-8')))191 addresses = [row['address'] for row in reader if 'address' in row]192 elif file.filename.endswith('.json'):193 addresses = [item['address'] for item in json.loads(contents) if 'address' in item]194 195 results = []196 for addr in addresses:197 res = get_coordinates(preprocess_address(addr))198 res.input_address = addr199 results.append(res.dict())200 return results201 202@app.post("/feedback/log-incorrect")203async def log_incorrect(data: dict):204 file_exists = os.path.isfile(FEEDBACK_FILE)205 with open(FEEDBACK_FILE, mode='a', newline='', encoding='utf-8') as f:206 writer = csv.DictWriter(f, fieldnames=data.keys())207 if not file_exists: writer.writeheader()208 writer.writerow(data)209 return {"status": "logged"}210 211@app.get("/feedback/download-csv")212async def download_incorrect_csv():213 if not os.path.exists(FEEDBACK_FILE):214 raise HTTPException(status_code=404, detail="File not found")215 return FileResponse(FEEDBACK_FILE, filename="errors.csv")216 217if __name__ == "__main__":218 uvicorn.run(app, host="0.0.0.0", port=7860)