/
Tikhonuk
/
veb
Обзор
Документация
Войти
/
Tikhonuk
/
veb
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
masterclass/geocoding.py
195 строк
6 KB
Tikhonuk
Дополнение
26 июн 2026, 09:18
26 июн 2026, 09:18
c704688
Код
Авторство
О чём код?
""" Геокодирование адресов для точного отображения на карте. Приоритет: 1. Яндекс.Геокодер (если задан YANDEX_GEOCODER_API_KEY в settings / .env) 2. OpenStreetMap Nominatim (резерв) 3. Без координат — iframe с mode=search и нормализованным адресом """ from __future__ import annotations import json import re import urllib.error import urllib.parse import urllib.request from typing import Optional, Tuple Coords = Tuple[float, float] _NOMINATIM_URL = "https://nominatim.openstreetmap.org/search" _USER_AGENT = "MasterClassPlatform/1.0 (educational django project)" def normalize_address(address: str) -> str: """ Подготавливает адрес для поиска на карте. «Г. Москва, Рябиновская, 32» → «Москва, улица Рябиновская, 32» """ parts = [p.strip() for p in address.split(",") if p.strip()] if not parts: return "" normalized: list[str] = [] for i, part in enumerate(parts): part = re.sub(r"^[Гг]\.\s*", "", part).strip() part = re.sub(r"^город\s+", "", part, flags=re.IGNORECASE).strip() is_street_name = ( len(parts) >= 3 and i == len(parts) - 2 and not any(ch.isdigit() for ch in part) ) has_street_keyword = bool( re.search( r"\b(ул\.?|улица|пр\.?|проспект|пер\.?|переулок|бульвар|б-р|ш\.|шоссе)\b", part, re.I, ) ) if is_street_name and not has_street_keyword: part = f"улица {part}" if i == len(parts) - 1 and part.isdigit() and len(parts) >= 2: part = f"дом {part}" normalized.append(part) return ", ".join(normalized) def _yandex_geocode(address: str) -> Optional[Coords]: from django.conf import settings api_key = getattr(settings, "YANDEX_GEOCODER_API_KEY", "") if not api_key: return None query = normalize_address(address) or address.strip() params = { "apikey": api_key, "geocode": query, "format": "json", "results": 1, "lang": "ru_RU", } encoded = urllib.parse.urlencode(params) for base_url in ( "https://geocode-maps.yandex.ru/v1/", "https://geocode-maps.yandex.ru/1.x/", ): request = urllib.request.Request(f"{base_url}?{encoded}") try: with urllib.request.urlopen(request, timeout=8) as response: data = json.loads(response.read().decode()) except (urllib.error.URLError, json.JSONDecodeError, TimeoutError): continue try: members = data["response"]["GeoObjectCollection"]["featureMember"] if not members: continue pos = members[0]["GeoObject"]["Point"]["pos"] lon, lat = pos.split() return float(lat), float(lon) except (KeyError, ValueError, IndexError, TypeError): continue return None def _nominatim_request(params: dict) -> Optional[Coords]: query = urllib.parse.urlencode(params) request = urllib.request.Request( f"{_NOMINATIM_URL}?{query}", headers={"User-Agent": _USER_AGENT, "Accept-Language": "ru"}, ) try: with urllib.request.urlopen(request, timeout=8) as response: data = json.loads(response.read().decode()) except (urllib.error.URLError, json.JSONDecodeError, TimeoutError): return None if not data: return None try: return float(data[0]["lat"]), float(data[0]["lon"]) except (KeyError, ValueError, IndexError): return None def geocode_address(address: str) -> Optional[Coords]: """Преобразует текстовый адрес в координаты (широта, долгота).""" address = address.strip() if not address: return None coords = _yandex_geocode(address) if coords: return coords normalized = normalize_address(address) parts = [p.strip() for p in address.split(",") if p.strip()] if len(parts) >= 2: city = re.sub(r"^[Гг]\.\s*", "", parts[0]).strip() if len(parts) >= 3: street = f"{normalize_address(address).split(',')[1].strip()} {parts[2]}" else: street = parts[1] coords = _nominatim_request( { "city": city, "street": street, "country": "Russia", "format": "json", "limit": 1, } ) if coords: return coords if normalized: coords = _nominatim_request({"q": f"{normalized}, Россия", "format": "json", "limit": 1}) if coords: return coords return _nominatim_request( {"q": address, "format": "json", "limit": 1, "countrycodes": "ru"} ) def build_yandex_map_embed_url( address: str = "", latitude: Optional[float] = None, longitude: Optional[float] = None, zoom: int = 17, ) -> str: """ URL для iframe Яндекс.Карт. С координатами — точная метка (ll + pt). Без координат — mode=search с нормализованным адресом и меткой на результате. """ if latitude is not None and longitude is not None: lon = f"{float(longitude):.6f}" lat = f"{float(latitude):.6f}" return ( f"https://yandex.ru/map-widget/v1/?ll={lon},{lat}" f"&z={zoom}&pt={lon},{lat},pm2rdm&lang=ru_RU" ) if address: search_text = normalize_address(address) or address.strip() text = urllib.parse.quote(search_text) return ( f"https://yandex.ru/map-widget/v1/?mode=search" f"&text={text}&z={zoom}&lang=ru_RU" ) return ""