归属判定升级: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,96 @@
|
||||
'use strict';
|
||||
/**
|
||||
* 把归属表里的组名归一到**地级**(市/州/盟/地区)。
|
||||
*
|
||||
* 为什么要这一步:Photon 的 `county` 字段层级并不稳定——
|
||||
* 「荔波小七孔」→ county=黔南州(地级)、city=荔波县(县级)✅
|
||||
* 「加榜梯田」 → county=从江县(县级)、city=摆别村(村级)❌
|
||||
* 直接拿 county 当组名,就会出现「加榜梯田」和「从江县」两个并列的组,
|
||||
* 而它们其实是同一片(黔南州)。
|
||||
*
|
||||
* 办法:对层级不对的条目,用该景点的坐标在「地级名单」里找最近的(≤120km),
|
||||
* 原来的县级名降为 detail(区县说明)。
|
||||
*
|
||||
* 用法:node tools/build-admin-normalize.cjs
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const cn = require('../lib/places-cn');
|
||||
const niche = require('../lib/places-niche').NICHE_SPOTS;
|
||||
|
||||
const file = path.join(__dirname, '..', 'lib', 'places-admin.js');
|
||||
|
||||
function haversine(lat1, lon1, lat2, lon2) {
|
||||
const R = 6371;
|
||||
const dLat = (lat2 - lat1) * Math.PI / 180;
|
||||
const dLon = (lon2 - lon1) * Math.PI / 180;
|
||||
const a = Math.sin(dLat / 2) ** 2
|
||||
+ Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) ** 2;
|
||||
return 2 * R * Math.asin(Math.min(1, Math.sqrt(a)));
|
||||
}
|
||||
|
||||
const isPrefecture = (n) => /(市|州|盟|地区)$/.test(String(n || '').trim());
|
||||
|
||||
function coordOf(group, name) {
|
||||
if (group === 'scenic') {
|
||||
const s = cn.EXTRA_SCENIC.find((x) => x[0] === name);
|
||||
return s ? [s[1], s[2]] : null;
|
||||
}
|
||||
const s = niche.find((x) => x[0] === name);
|
||||
return s ? [s[1], s[2]] : null;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const src = fs.readFileSync(file, 'utf8');
|
||||
const SPOT_AREA = JSON.parse(src.match(/const SPOT_AREA = (\{.*?\});/s)[1]);
|
||||
const AREA_COORD = JSON.parse(src.match(/const AREA_COORD = (\{.*?\});/s)[1]);
|
||||
|
||||
// 地级名单:城市库 + 归属表里层级正确的地名
|
||||
const prefectures = new Map();
|
||||
for (const [n, la, lo] of cn.CN_CITIES) prefectures.set(n, [la, lo]);
|
||||
for (const [n, coord] of Object.entries(AREA_COORD)) {
|
||||
if (isPrefecture(n) && coord) prefectures.set(n, coord);
|
||||
}
|
||||
|
||||
let fixed = 0;
|
||||
let unresolved = 0;
|
||||
for (const [key, entry] of Object.entries(SPOT_AREA)) {
|
||||
if (isPrefecture(entry.area)) continue;
|
||||
const [group, name] = [key.slice(0, key.indexOf(':')), key.slice(key.indexOf(':') + 1)];
|
||||
const coord = coordOf(group, name);
|
||||
if (!coord) continue;
|
||||
let best = null;
|
||||
for (const [n, c] of prefectures) {
|
||||
const d = haversine(coord[0], coord[1], c[0], c[1]);
|
||||
if (!best || d < best.d) best = { n, d };
|
||||
}
|
||||
if (!best || best.d > 120) { unresolved += 1; continue; }
|
||||
entry.detail = entry.detail || entry.area;
|
||||
entry.area = best.n;
|
||||
fixed += 1;
|
||||
}
|
||||
|
||||
const body = `'use strict';
|
||||
/**
|
||||
* 内置地点库的归属地名(自动生成,勿手改)。
|
||||
*
|
||||
* 生成方式:
|
||||
* tools/build-admin.cjs —— 首轮,Photon 按名检索 + 坐标距离消歧
|
||||
* tools/build-admin-fix.cjs —— 二轮,去后缀/核心词重试,补首轮搜不到的
|
||||
* tools/build-admin-normalize.cjs —— 三轮,把组名归一到地级(市/州/盟/地区)
|
||||
*
|
||||
* 字段:
|
||||
* SPOT_AREA { 'scenic:荔波小七孔': { area, detail, state, level } }
|
||||
* AREA_COORD { '荔波县': [lat, lon] } 各地名代表坐标,运行时做最近市兜底
|
||||
*/
|
||||
const SPOT_AREA = ${JSON.stringify(SPOT_AREA)};
|
||||
|
||||
const AREA_COORD = ${JSON.stringify(AREA_COORD)};
|
||||
|
||||
module.exports = { SPOT_AREA, AREA_COORD };
|
||||
`;
|
||||
fs.writeFileSync(file, body, 'utf8');
|
||||
console.log('fixed', fixed, 'unresolved', unresolved, 'entries', Object.keys(SPOT_AREA).length);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user