归属判定升级:DataV 官方行政区划表 + 同名县消歧
用阿里 DataV GeoAtlas(免密钥)替换 OSM Overpass 离线县表: - 民政部 adcode 体系,县级前 4 位即所属地级码,2832 县 100% 可定位地级 (OSM 表只有 1182/2362,且 admin_level 层级不可靠) - 同名县(全国 30 组)保留全部候选,运行时按坐标消歧, 修掉「柳州的城中区被归到西宁市」 归属判定改为「按名字精确查官方表」优先于「按中心点猜最近的县」: 窄长县按中心点会跨省错判(环江喀斯特实际在环江县,按中心点判到荔波县) 查表带距离校验,精确全称命中放宽到 150km(狭长县中心离边界可达 85km+) 修掉三个真实缺陷: - Overpass 兜底把地级塞进 osmCounty、县名塞进 osmCity, 与 attachPlaces「先找县级」的顺序相反,导致兜底路径永远丢下层县名 - Overpass 请求无硬超时,公共实例「连着不返回」能把一次分析拖成十几分钟; 补三层限(单次超时 / 阶段总时限 / is_in 反查次数上限),13 分钟 → 48 秒 - Photon 的 osm.place.city 类目混着景点 POI,kind==='城市' 时无条件把名字 当地名,导致「社更穿洞北门」「柳州之门」等景点自己成一组; 改为必须同时是行政区名且能在官方表查到 新增离线回归测试(都不打外部网络,Photon/Overpass 挂掉也能回归): - test-admin-lookup:同名县消歧、已知边界点、狭长县远端点(19 项) - test-overpass-fallback:本地假 Overpass,验兜底路径、简称归一、200+HTML 错误页 - test-grouping-regression:本地假 Photon,20 候选完整分组 - verify-ui-full:端到端 UI 验收(真实无头 Chrome 读真实 DOM)
This commit is contained in:
@@ -35,7 +35,7 @@ node server.js # 或 npm start
|
||||
| 步骤 | 用户操作 | 系统行为 |
|
||||
| --- | --- | --- |
|
||||
| ① 出行条件 | 输入地名联想 / 浏览器定位、选日期区间、单程时长上限、搜索半径、是否排除同城、交通与兴趣偏好 | 地理编码 → 检索周边候选 → 批量取天气与 AQI → 公路矩阵 + 铁路可达性 → 评分排序 |
|
||||
| ② 备选清单 | 查看方位罗盘、评分环、逐日天气、推荐理由、数据来源说明;勾选目的地 | 默认预选 Top 3 并给出提示;支持按评分/天气/距离/单程时长排序;可「只看小众」、可「对比已选」并排看关键指标 |
|
||||
| ② 备选清单 | 查看方位罗盘、评分环、逐日天气、推荐理由、数据来源说明;按地名分组浏览、搜索目的地;勾选目的地 | 默认预选 Top 3 并给出提示;支持按评分/天气/距离/单程时长排序;按「地名 → 景点」两级分组;可搜索、可「只看小众」、可「对比已选」并排看关键指标 |
|
||||
| ③ 行程规划 | 填写具体需求(同行人、预算、体力、禁忌等),点击生成;生成后可继续提要求迭代 | 清单数据 + 需求 + 本站预设提示词 → OpenAI 兼容接口流式输出 Markdown 方案 |
|
||||
|
||||
页面右上角「设置」分两块:**AI 接口**(地址、密钥、模型,含常用供应商预设、预设提示词查看/复制、强制演示模式)与 **数据源增强**(高德 Web 服务 Key)。两者都只存在浏览器 `localStorage`,随请求传给本地服务代理。
|
||||
@@ -109,6 +109,100 @@ node server.js # 或 npm start
|
||||
- Photon 分类检索用 `bbox + 分类` 作为稳定缓存键(公共实例限流,重复分析必须复用缓存)。
|
||||
- 候选上限 28 个,按「类型轮转取样」抽取,保证小众点位不会被热门点位挤掉。
|
||||
|
||||
### 归属地名与分组
|
||||
|
||||
第二步把候选按「**地名 → 景点**」两级展示,所以每个候选都要先解析出它属于哪个地名。
|
||||
|
||||
| 来源 | 地名来源 | 层级 |
|
||||
| --- | --- | --- |
|
||||
| 内置城市库 | 名字本身就是地名(柳州市) | 地级 |
|
||||
| 高德 POI | `cityname`(地级市)+ `adname`(区县) | 地级 + 区县 |
|
||||
| 内置知名 / 小众库 | 离线归属表 `lib/places-admin.js` | 地级 + 区县 |
|
||||
| Photon / Overpass (OSM) | **按 OSM 自带的县名字段查官方表** `lib/places-admin-dv.js` | 地级 + 区县 |
|
||||
| 以上都拿不到 | 按坐标找最近的县 → 推出地级(官方县表,≤60km) | 地级 + 区县 |
|
||||
|
||||
铁律:
|
||||
|
||||
- **组名必须落在地级**(市 / 州 / 盟 / 地区)。用街道名或村名当组名等于没分组——同一个县下面的几十个点会被拆成几十组。
|
||||
- **Photon 的 `osm.place.city` 类目里混着景点 POI**(牌坊、寺庙、老街),它们的名字也带「市 / 州」后缀。不能因为 `kind === '城市'` 就把名字当地名——会出现「社更穿洞北门」自己成一组这种荒唐结果。必须同时满足「名字是行政区名」且「能在官方地级表里查到」才当地名节点,否则走正常归属流程。
|
||||
- **OSM 中国的字段层级与直觉相反**:`county` 是地级市/州,`city` 是县/区,`district` 是街道,`locality` 是村。
|
||||
- **优先按名字精确查官方表,别靠「按坐标猜最近的县」**。窄长县按中心点距离会判到隔壁省去:「环江喀斯特世界自然遗产」实际在环江县(河池市),按中心点却会判到 45km 外的荔波县(黔南州)——差一级省。而 OSM 的 `addr:county` 在多数条目上直接写了标准县名(`环江毛南族自治县`),按名字查是准的。
|
||||
- 查表**必须带距离校验**:同名县离候选点太远(>80km)就判为不可信,退回按坐标近邻。
|
||||
- 查表顺序:先在 `county` / `city` / `district` 三个字段里找**县级**(拿到下层地名),都找不到再退地级。OSM 常常把地级填进 `county`、县名填进 `city`(「黔南州」+「荔波县」),只看第一个字段就定案会丢掉下层地名。
|
||||
- **不要信 OSM 自己填的归属字段的层级**。Photon 的 `city` 字段会给出「乐依村」「陈家甫」这种村名当区县——所以查到的东西必须经 `isDistrictName()`(县/区/旗/盟/州/市)过滤,村名一律丢弃。
|
||||
- **OSM 的 `admin_level` 在中国是 L4=省 / L5=地级市 / L6=县**,不是 L4/L6/L8。查 `admin_level~"^[468]$"` 会正好跳过地级市那层。
|
||||
- 分组键会剥掉行政后缀(「柳州」与「柳州市」归一组),民族自治州要连民族名一起去掉(「黔南布依族苗族自治州」→「黔南州」)。
|
||||
- 高德 `regeo` 的**批量查询不可用**(`batch=true` + `locations`,无论分隔符与参数顺序都返回 `INVALID_PARAMS`),必须逐点反查;并发 3 会触发限流,失败的候选会退回街道名当组名,因此用**串行 + 60ms 间隔**。
|
||||
|
||||
#### 离线行政区划表(阿里 DataV GeoAtlas)
|
||||
|
||||
| 表 | 条数 | 作用 |
|
||||
| --- | --- | --- |
|
||||
| `lib/places-admin.js` | 323 | 内置知名 / 小众地点的归属(地级 + 区县) |
|
||||
| `lib/places-admin-dv.js` | 2832 县 / 343 地级 | **官方行政区划表**:按名字查县/地级,运行时归属判定的主力 |
|
||||
|
||||
数据来自[阿里 DataV GeoAtlas](https://datav.aliyun.com/portal/school/atlas/area_selector)(免密钥、免鉴权),用的是国家民政部 **adcode** 体系:省级 2 位 / 地级 4 位 / 县级 6 位,**县级前 4 位即所属地级码**(451226 环江县 → 4512 河池市)——归属关系是**编码里写死的**,不需要推断。
|
||||
|
||||
```bash
|
||||
# 1) 拉原始数据(三级下钻:省 → 地级市 → 县),输出到 C:/tmp/datav-admin.json
|
||||
node tools/fetch-datav-admin.cjs
|
||||
# 2) 生成运行时表
|
||||
node tools/build-admin-dv.cjs C:/tmp/datav-admin.json
|
||||
```
|
||||
|
||||
> 早期版本用的是 Overpass 拉的 OSM 县表(`lib/places-county.js` / `lib/places-prefecture.js`),**已废弃**:OSM 中国的 `admin_level` 层级不可靠、编码只有约 50% 覆盖(1182/2362 个县查不到所属地级),且公共 Overpass 实例限流极狠(大查询后连 1KB 的小查询都 502/504)。DataV 版 2832 个县**全部**能定位到所属地级。
|
||||
|
||||
几个必须记住的坑:
|
||||
|
||||
- **同名县要保留全部候选**。全国有 **30 组**同名县(「城中区」柳州/西宁都有、「鼓楼区」南京/徐州/福州/开封都有、「铁西区」沈阳/鞍山/四平都有、「市中区」济南/枣庄/内江/乐山都有)。用县名做单一字典键会互相覆盖——柳州的「城中区」会被归到西宁市。所以表里同名县的值是**数组**,运行时按坐标距离选最近的那个。
|
||||
- **省直辖县级单位不是地级市**。DataV 把海南的东方市/昌江黎族自治县、新疆兵团的石河子市、湖北的仙桃市等标成 `level=city`,靠 **adcode 是否以 `00` 结尾**区分(真地级市是 xx00,省直辖县级是 9xxx 段)。不按这个分,归属会整整差一级。
|
||||
- **剥民族名要用显式清单**,不能用 `[一-龥]{1,3}族` 这种贪婪正则——「环江毛南族自治县」会被吃成「环自治县」。而且顺序很重要:先剥「自治县/州」尾巴,民族名块才会露在末尾。
|
||||
- **别名表要县/地分开**(`COUNTY_ALIAS` → 县全称,`PREF_ALIAS` → 地级简称)。混在一张表里会因为「同名县/市」互相覆盖而查错。
|
||||
|
||||
#### 兜底路径:Photon 全挂时走 Overpass
|
||||
|
||||
| 步骤 | 实现 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 1. 取候选 | `overpassQuery()` | 按 `OVERPASS_TAGS` 逐类查 OSM 原始标签 |
|
||||
| 2. 取行政区 | `overpassAdminAreas()` | `out center tags` 取 bbox 内 L5/L6 行政区的**中心点** |
|
||||
| 3. 判归属 | `areaByPoint()` | 按中心点做近邻判定 |
|
||||
| 4. 精确反查 | `overpassAdminAtPoint()` | 近邻判定拿不到时逐点用 `is_in` 反查 |
|
||||
| 5. 归一 | `lookupAdminByName()` | OSM 的简称/全称统一过官方表,同名县按距离消歧 |
|
||||
|
||||
关键事实:
|
||||
|
||||
- **relation 级 geometry 在 Overpass 里拿不到**。`out tags geom` 和 `out geom` 都不返回 relation 级的 `geometry` 数组(几何在 `members` 里)。所以不要走「拉多边形 + 本地点面判定」这条路。
|
||||
- **`is_in` 反查一次就能拿到完整层级**:`[out:json];is_in(lat,lon)->.a;.a out tags;` 返回 L4 省 / L5 地级 / L6 县(实测 25.1,107.98 → 环江县 / 河池市 / 广西)。注意 `rel(pivot.a)` 拿不到点→区映射,要直接输出 area 集合。
|
||||
- **公共实例必须多镜像轮换**,且要判定响应体是否真是 JSON:实例可能返回 **200 + HTML 错误页**(如 `duplicate_query`),只看 `res.status` 会把错误页当成功。判据是响应体是否以 `{` 开头。
|
||||
- **必须有硬超时和总时限预算**。公共实例经常「连着不返回」(不报错也不结束),没有 `AbortSignal.timeout` 的话一次兜底能把分析流程拖成十几分钟(实测踩过)。三处都要限:
|
||||
- 单次请求超时(`SEARCH.overpassTimeoutMs`,默认 20s);
|
||||
- 单个阶段的总时限(镜像轮询不超过 2 倍超时);
|
||||
- 逐点 `is_in` 反查的次数上限(`SEARCH.overpassIsInMax`,默认 8)——这是最慢的一环,超限后改用 bbox 行政区中心点近邻判定。
|
||||
- 超时后**候选照常入池**(归属还有离线官方表兜底),不能因为反查超时就丢掉整个候选。
|
||||
- Overpass 的兴趣点大多**没有 `addr:*` 标签**,直接拿归属必然得到空组名——必须靠 `is_in` 反查。
|
||||
|
||||
#### 离线回归测试
|
||||
|
||||
归属逻辑不能只靠真实网络验证——Photon / Overpass 随时会挂,挂了就没法回归。仓库里有三个**不发外部请求**的离线测试:
|
||||
|
||||
```bash
|
||||
node tools/test-admin-lookup.cjs # 归属判定单测:同名县消歧、已知边界点、狭长县远端点
|
||||
node tools/test-overpass-fallback.cjs # 起本地假 Overpass:验兜底路径、简称归一、200+HTML 错误页
|
||||
node tools/test-grouping-regression.cjs # 起本地假 Photon:20 个候选的完整分组回归
|
||||
```
|
||||
|
||||
另有端到端 UI 验收(起假 Photon + 真实无头 Chrome,读真实 DOM):
|
||||
|
||||
```bash
|
||||
node tools/verify-ui-full.cjs # 分组 / undefined / 空组名 / 搜索 / 折叠 + 截图
|
||||
```
|
||||
|
||||
写这类测试时注意:**`cfg.SEARCH.maxCandidates` 缺省会静默返回 0 候选**(`selected.length < undefined` 恒假);**第二轮跑要清 `require.cache` 或换 origin**,否则命中第一轮缓存。
|
||||
|
||||
拉取时的坑:一次拉全国必然 502(5 个镜像全挂),必须按 10°×15° 分块;用 `out center` 而不是 `out geom`(几何版体积大十几倍,更容易超时)。境外行政区会混进同一层级数据,靠**有无 `division_code` 或 `name` 本身是中文**来剔除。
|
||||
|
||||
县级表第三项是**所属地级名**,由 GB/T 2260 编码前缀推出(县级码前 4 位即地级码:荔波县 `522722` → `5223` 黔南州)。这比「按距离找最近地级」精确得多——桂黔边境荔波县与河池市相距不到 40km,按中心点距离会分错。
|
||||
|
||||
### 评分口径
|
||||
|
||||
总分 0~100,由六项加权相加:天气舒适度 `0~40`、时间适配 `0~26`、兴趣匹配 `0~14`、距离 `0~7`、目的地辨识度 `0~8`、空气质量 `0~6`。整段行程都是坏天气时会触发硬门槛,不允许给出「强烈推荐」。每张卡片都带 `breakdown`、`reasons`、`warnings`,可解释。
|
||||
@@ -138,6 +232,9 @@ node server.js # 或 npm start
|
||||
| BigDataCloud / Photon Reverse | 浏览器定位后的反向地理编码 | 免 | 提示手动输入地名 |
|
||||
| 内置 `lib/places-cn.js` | 全国知名景区 + 主要城市坐标库 | — | 始终参与,保证任何城市都有可用清单 |
|
||||
| 内置 `lib/places-niche.js` | 206 条小众地点:古村、遗址、湿地、地质景观、洞穴、小馆、徒步线 | — | 始终参与,并保证约 30% 的候选名额 |
|
||||
| 内置 `lib/places-admin.js` | 内置地点的归属地名(323 条,离线生成) | — | 始终参与;缺失时按坐标回退到最近地级市 |
|
||||
| 内置 `lib/places-admin-dv.js` | 官方行政区划表:2832 县 / 343 地级(DataV GeoAtlas,离线生成) | — | 始终参与;同名县按坐标消歧,查不到时退到按坐标近邻 |
|
||||
| Overpass (OpenStreetMap) | Photon 全挂时的候选兜底(免密钥),多镜像轮换 | 免 | 跳过并注明「实时检索不可用」;实例返回 200 但内容是 HTML 错误页时同样判为失败 |
|
||||
| 高德 Web 服务 `geocode/geo` / `place/text` / `place/around`(含 `types=100000` 住宿)/ `direction/driving` / `staticmap`(路线图真实底图) | 中文地名检索、实时周边 POI、周边住宿参考、国内驾车路径、地图图层 | `AMAP_KEY` 或页面「设置 → 数据源增强」 | 静默跳过并退回 Open-Meteo / OSRM / 内置库 / 方位示意图;Key 无效会在页面顶部或设置里直接报错 |
|
||||
| RollingGo `rgh` CLI(本机安装) | 酒店实时参考价、星级、距离、可预订链接 | `npm i -g @rollinggo/hotel` + `rgh login` | 跳过,退回高德 POI;只调用只读子命令 |
|
||||
| 铁路接口 | 真实高铁/城际耗时与票价 | `RAIL_API_URL` | 降级为里程-速度估算并标注 |
|
||||
|
||||
Reference in New Issue
Block a user