dc63632e77
- app/utils/geo.py: reverse_geocoder 单例(mode=1 单进程 KDTree),经纬度→最近聚居点 - app/utils/meituan_city.py: 坐标→美团 city_id(省份/城市名桥接 + 多级兜底 + lru_cache 量化) - feed(rec) / top-sales: 按解析出的 city_id 过滤离线库;城市解析不出 / 老客户端不带坐标 → degraded - top-sales 与 rec 一致置空库内距离(相对城市默认点,对用户无意义) - main.py 启动预热 KDTree;pyproject 加 reverse_geocoder 依赖 + 分发 city_dict.txt - 新增 geo / meituan_city 测试(56 例);scripts/load_meituan_coupon_tsv.py 灌样本到本地 SQLite - .gitignore 忽略样本 TSV 与 .claude 本地设置 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
66 lines
3.1 KiB
Python
66 lines
3.1 KiB
Python
"""通过经纬度反查城市(reverse_geocoder 离线库,零网络调用)。
|
||
|
||
reverse_geocoder 内置 ~2.5M 条全球城市/聚居点的经纬度→地名映射表,
|
||
构建一次 KDTree(~几十MB 内存)后,查询为纯内存搜索,不作任何外部网络调用。
|
||
|
||
⚠️ 必须持有单例、且用 mode=1(单进程):
|
||
- reverse_geocoder 的模块级 rg.search()/rg.get() **每次调用都会 new 一个 RGeocoder**,
|
||
即每次都重新解析 ~2.5M 行 CSV + 重建 KDTree(数秒/次)。绝不能在服务端按请求调用。
|
||
- 默认 mode=2 用 multiprocessing 按 CPU 数 spawn 子进程做并行查询;在服务端 / Windows
|
||
spawn 下会重复 import 主模块(无 __main__ guard 时直接报错),既慢又危险。
|
||
故本模块持有一个 mode=1 的 RGeocoder 单例,建一次树、复用;查询走单进程内存搜索。
|
||
|
||
⚠️ 精度说明:gazetteer 里的中国数据粒度不一致——直辖市/省会通常直接命中城市名,
|
||
但部分城市会命中到区/街道级(如天津→Erwangzhuang、西安→Zhangjiabao),
|
||
此时 admin1(省级行政区)可作为回退。业务侧建议优先用 admin1 做城市级判定。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
import reverse_geocoder as rg # type: ignore[import-untyped]
|
||
|
||
# mode=1 单进程 KDTree 的单例;None 表示尚未构建(见 ensure_loaded)。
|
||
_geocoder: rg.RGeocoder | None = None
|
||
|
||
|
||
def ensure_loaded() -> None:
|
||
"""构建(或复用)RGeocoder 单例(幂等)。
|
||
|
||
首次调用解析 ~2.5M 行 CSV + 构建 KDTree(~秒级、数十 MB)。生产应在 main.py 的
|
||
lifespan 启动阶段主动调用一次,把这份一次性成本摊到启动,避免砸在第一个
|
||
/feed?tab=rec / /top-sales 请求上。mode=1 = 单进程,不 spawn 子进程。
|
||
"""
|
||
global _geocoder
|
||
if _geocoder is None:
|
||
_geocoder = rg.RGeocoder(mode=1, verbose=False)
|
||
|
||
|
||
def get_city(latitude: float, longitude: float) -> dict[str, str]:
|
||
"""根据经纬度反查最近聚居点。
|
||
|
||
返回 dict:
|
||
- name: 最近聚居点名称(英文),如 "Beijing" / "Fengsheng";
|
||
中国境内可能是区/街道级;海洋/无人区返 ""
|
||
- admin1: 省级行政区(英文),如 "Beijing" / "Hubei" / "Chongqing Shi";
|
||
直辖市 admin1 即为城市名
|
||
- country: ISO 3166-1 alpha-2,如 "CN"
|
||
- latitude: 匹配到的参考点纬度(字符串)
|
||
- longitude: 匹配到的参考点经度(字符串)
|
||
|
||
未匹配到(海洋/远洋)时返回空字符串字段。
|
||
"""
|
||
ensure_loaded()
|
||
assert _geocoder is not None # ensure_loaded 保证已构建
|
||
results: list[dict[str, Any]] = _geocoder.query([(latitude, longitude)])
|
||
if not results:
|
||
return {"name": "", "admin1": "", "country": "", "latitude": "", "longitude": ""}
|
||
r = results[0]
|
||
return {
|
||
"name": str(r.get("name", "")),
|
||
"admin1": str(r.get("admin1", "")),
|
||
"country": str(r.get("cc", "")),
|
||
"latitude": str(r.get("lat", "")),
|
||
"longitude": str(r.get("lon", "")),
|
||
}
|