归属判定升级: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:
@@ -0,0 +1,86 @@
|
||||
'use strict';
|
||||
/**
|
||||
* 归属判定离线单测(不发网络请求)。
|
||||
*
|
||||
* 覆盖三件事:
|
||||
* 1. 同名县按坐标消歧(城中区:柳州 vs 西宁)
|
||||
* 2. 已知边界点归属正确(环江喀斯特→河池市/环江县,不许跑到荔波县)
|
||||
* 3. 下层地名是**县**,不是村/街道
|
||||
*/
|
||||
const { attachPlaces } = require('../lib/sources');
|
||||
|
||||
const CASES = [
|
||||
// [名称, lat, lon, 期望地级, 期望下层县]
|
||||
['环江喀斯特世界自然遗产-核心功能区', 25.1, 108.28, '河池市', '环江毛南族自治县'],
|
||||
['大化瑶族自治县七百弄', 24.35, 107.99, '河池市', '大化瑶族自治县'],
|
||||
['都安瑶族自治县', 24.09, 108.10, '河池市', '都安瑶族自治县'],
|
||||
['荔波小七孔', 25.25, 107.88, '黔南州', '荔波县'],
|
||||
['柳州钢铁厂旧址', 24.33, 109.42, '柳州市', '城中区'],
|
||||
['西宁某点', 36.62, 101.78, '西宁市', '城中区'],
|
||||
['鼓楼区南京点', 32.06, 118.77, '南京市', '鼓楼区'],
|
||||
['福州鼓楼点', 26.08, 119.30, '福州市', '鼓楼区'],
|
||||
['徐州鼓楼点', 34.26, 117.19, '徐州市', '鼓楼区'],
|
||||
];
|
||||
|
||||
(async () => {
|
||||
// 构造候选:带 OSM 归属字段,模拟真实场景里 attachPlaces 走的那条分支
|
||||
const cands = CASES.map(([name, lat, lon]) => ({
|
||||
name, lat, lon, kind: '景区',
|
||||
osmCounty: '', osmCity: '', osmDistrict: '',
|
||||
}));
|
||||
|
||||
// 第一轮:喂「OSM 自带的县名字段」
|
||||
const withNames = [
|
||||
['环江喀斯特世界自然遗产-核心功能区', '环江毛南族自治县'],
|
||||
['大化瑶族自治县七百弄', '大化瑶族自治县'],
|
||||
['都安瑶族自治县', '都安瑶族自治县'],
|
||||
['荔波小七孔', '荔波县'],
|
||||
['柳州钢铁厂旧址', '城中区'],
|
||||
['西宁某点', '城中区'],
|
||||
['鼓楼区南京点', '鼓楼区'],
|
||||
['福州鼓楼点', '鼓楼区'],
|
||||
['徐州鼓楼点', '鼓楼区'],
|
||||
];
|
||||
withNames.forEach((c, i) => { cands[i].osmCounty = c[1]; });
|
||||
|
||||
await attachPlaces(cands, {});
|
||||
|
||||
let pass = 0;
|
||||
let fail = 0;
|
||||
CASES.forEach(([name, , , expArea, expDetail], i) => {
|
||||
const c = cands[i];
|
||||
const okArea = c.place === expArea;
|
||||
const okDetail = c.placeDetail === expDetail;
|
||||
if (okArea && okDetail) {
|
||||
pass += 1;
|
||||
console.log(` ✓ ${name} → ${c.place} / ${c.placeDetail}`);
|
||||
} else {
|
||||
fail += 1;
|
||||
console.log(` ✗ ${name} → 实际 ${c.place} / ${c.placeDetail || '(空)'},期望 ${expArea} / ${expDetail}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 第二轮:不给任何 OSM 字段,纯靠坐标兜底(应落到最近县,仍不应出现空组名)
|
||||
const bare = CASES.map(([name, lat, lon]) => ({ name, lat, lon, kind: '景区' }));
|
||||
await attachPlaces(bare, {});
|
||||
console.log(' --- 无 OSM 字段(坐标兜底)');
|
||||
bare.forEach((c, i) => {
|
||||
const ok = c.place && c.place !== '?' && c.placeDetail !== '?';
|
||||
if (ok) pass += 1; else fail += 1;
|
||||
console.log(` ${ok ? '✓' : '✗'} ${CASES[i][0]} → ${c.place || '(空)'} / ${c.placeDetail || '(空)'}`);
|
||||
});
|
||||
|
||||
// 第三轮:狭长县的远端点。大化县中心点在西北角,东南角的响水关离中心 85km,
|
||||
// 按 80km 一刀切会把**正确的**县名丢掉。精确全称命中要放宽阈值。
|
||||
console.log(' --- 狭长县远端点');
|
||||
{
|
||||
const c = { name: '响水关', lat: 24.7217, lon: 107.6123, kind: '景区', osmCounty: '大化瑶族自治县' };
|
||||
await attachPlaces([c], {});
|
||||
const ok = c.place === '河池市' && c.placeDetail === '大化瑶族自治县';
|
||||
if (ok) pass += 1; else fail += 1;
|
||||
console.log(` ${ok ? '✓' : '✗'} 响水关 → ${c.place} / ${c.placeDetail || '(空)'}`);
|
||||
}
|
||||
|
||||
console.log(`\n通过 ${pass} / 失败 ${fail}`);
|
||||
process.exit(fail ? 1 : 0);
|
||||
})();
|
||||
Reference in New Issue
Block a user