用阿里 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)
157 lines
7.3 KiB
JavaScript
157 lines
7.3 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* 由 DataV 行政区划原始数据生成离线归属表。
|
||
*
|
||
* 产出 lib/places-admin-dv.js:
|
||
* COUNTY_DV 县级 → [纬度, 经度, 所属地级名, adcode]
|
||
* PREF_DV 地级 → [纬度, 经度, adcode]
|
||
*
|
||
* 为什么不用 OSM 那套:OSM 中国的 admin_level 层级不可靠、编码只有 50% 覆盖,
|
||
* 只能靠「按中心点猜」;DataV 用的是国家民政部 adcode 体系,
|
||
* 县级前 4 位就是所属地级码,**100% 准确**,不需要猜。
|
||
*
|
||
* 用法:node tools/build-admin-dv.cjs C:/tmp/datav-admin.json
|
||
*/
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const { shortPrefecture } = require('./lib-short-name.cjs');
|
||
|
||
const src = process.argv[2] || 'C:/tmp/datav-admin.json';
|
||
const out = path.join(__dirname, '..', 'lib', 'places-admin-dv.js');
|
||
|
||
const d = JSON.parse(fs.readFileSync(src, 'utf8'));
|
||
|
||
// 地级:level==city 且 adcode 以 00 结尾的条目。
|
||
// 键统一用简称(「黔南布依族苗族自治州」→「黔南州」),与运行时 placeKey 一致。
|
||
//
|
||
// **关键区分**:DataV 把「省直辖县级单位」也标成 level=city
|
||
// (海南的东方市/昌江黎族自治县、新疆兵团的石河子市、湖北的仙桃市…),
|
||
// 但它们的 adcode 不以 00 结尾(46**9001**、65**9001**、42**9001**)。
|
||
// 行政区划代码里「地级市用 xx00、省直辖县级用 9xxx 段」是固定规则,
|
||
// 不按这个分就会把县级市当成地级市,归属会整整差一级。
|
||
const prefecture = {};
|
||
const prefectureFull = {};
|
||
for (const c of d.cities) {
|
||
if (c.level !== 'city') continue;
|
||
if (!c.centroid) continue;
|
||
if (!/00$/.test(c.adcode)) continue;
|
||
const short = shortPrefecture(c.name);
|
||
prefecture[short] = [c.centroid[1], c.centroid[0], c.adcode];
|
||
prefectureFull[short] = c.name;
|
||
}
|
||
|
||
// 省级直辖县级单位(海南的东方市、新疆兵团的石河子市…):行政上是「县」,
|
||
// 上级直接是省,没有地级市这一层 —— 所以它们的归属地级就是省名。
|
||
const directCounties = [];
|
||
for (const c of d.cities) {
|
||
if (!c.centroid) continue;
|
||
const isCityLevel = c.level === 'city';
|
||
if (isCityLevel && /00$/.test(c.adcode)) continue; // 真正的地级市,上面已处理
|
||
directCounties.push(c);
|
||
}
|
||
|
||
// 县级 → 地级
|
||
//
|
||
// **同名县必须保留全部候选**:全国有 22 组同名县(「城中区」柳州/西宁都有、
|
||
// 「鼓楼区」南京/徐州/福州/开封都有、「铁西区」沈阳/鞍山/四平都有)。
|
||
// 只留一个会把柳州的点归到西宁去。
|
||
// 所以同名时值是**数组**,运行时按坐标距离选最近的那个。
|
||
const county = {};
|
||
// 注意:county[name] **永远**是数组(同名县有多个候选)。
|
||
// 不能写成「单值,冲突时升级成数组」——单值本身也是数组,
|
||
// Array.isArray 会把「第一次写入的条目」误判成「同名组」,
|
||
// 结果第二个同名县被 push 进第一个县的四元组里,表直接被污染。
|
||
function addCounty(name, entry) {
|
||
if (!county[name]) county[name] = [];
|
||
// 同 adcode 视为同一条(例如直辖县级既在 cities 又在 counties 里出现)
|
||
if (county[name].some((e) => e[3] === entry[3])) return;
|
||
county[name].push(entry);
|
||
}
|
||
for (const c of d.counties) {
|
||
if (!c.centroid) continue;
|
||
// 地级名统一简称化(「黔南布依族苗族自治州」→「黔南州」)
|
||
const pref = shortPrefecture(c.city || c.province || '');
|
||
addCounty(c.name, [c.centroid[1], c.centroid[0], pref, c.adcode]);
|
||
}
|
||
// 直辖县级(北京/上海/重庆/港澳台等)由省级文件直接给出
|
||
for (const c of directCounties) {
|
||
const pref = shortPrefecture(c.province || '');
|
||
addCounty(c.name, [c.centroid[1], c.centroid[0], pref, c.adcode]);
|
||
if (!prefecture[pref]) {
|
||
prefecture[pref] = [c.centroid[1], c.centroid[0], c.provinceAdcode];
|
||
prefectureFull[pref] = pref;
|
||
}
|
||
}
|
||
// 只有一组的压回单值,减少运行时分支;多组保持数组
|
||
for (const k of Object.keys(county)) {
|
||
if (county[k].length === 1) county[k] = county[k][0];
|
||
}
|
||
|
||
// 名称索引:允许用「短名/别名」查
|
||
// DataV 只给全称,但 OSM/高德常写「环江县」「荔波县」「罗城」这类简称或去族名写法,
|
||
// 统一在这里做归一,避免运行时各写一套。
|
||
// 分成两张表:县级别名 → 县全称;地级别名 → 地级简称。
|
||
// 混在一张表里会因为「同名县/市」互相覆盖而查错。
|
||
const countyAlias = {};
|
||
const prefAlias = {};
|
||
function regAlias(table, key, full) {
|
||
if (!full || !key || key === full) return;
|
||
if (!table[key]) table[key] = full;
|
||
}
|
||
/**
|
||
* 剥掉地名里的民族名块。
|
||
* 不能用 `[一-龥]{1,3}族` 这种贪婪正则——「环江毛南族自治县」会把
|
||
* 「江毛南族」整段吃掉变成「环自治县」(实测)。必须用**已知的民族名清单**。
|
||
* 顺序很重要:先去掉「自治县/自治州」尾巴,民族名块才会露在末尾。
|
||
*/
|
||
const ETHNIC_LIST = require('./lib-short-name.cjs').ETHNIC;
|
||
function stripEthnic(name) {
|
||
let s = name.replace(/自治(县|旗|州|区)$/, '');
|
||
let changed = true;
|
||
while (changed) {
|
||
changed = false;
|
||
for (const eth of ETHNIC_LIST) {
|
||
if (s.endsWith(eth)) { s = s.slice(0, -eth.length); changed = true; }
|
||
}
|
||
}
|
||
return s.replace(/(?:[\u4e00-\u9fa5]{1,3}族)+$/, '');
|
||
}
|
||
for (const name of Object.keys(county)) {
|
||
const noEthnic = stripEthnic(name); // 环江毛南族自治县 → 环江
|
||
regAlias(countyAlias, noEthnic, name);
|
||
regAlias(countyAlias, noEthnic + '县', name); // → 环江县
|
||
regAlias(countyAlias, noEthnic + '旗', name);
|
||
regAlias(countyAlias, name.replace(/自治(县|旗)$/, (m, g) => g), name); // 环江毛南族县
|
||
regAlias(countyAlias, name.replace(/自治(县|旗)$/, ''), name); // 环江毛南族
|
||
}
|
||
for (const [name, full] of Object.entries(prefectureFull)) {
|
||
// 值统一是地级**简称**(PREF_DV 的键),否则运行时查 PREF_DV 会落空
|
||
regAlias(prefAlias, full, name); // 黔南布依族苗族自治州 → 黔南州
|
||
regAlias(prefAlias, stripEthnic(full), name);
|
||
regAlias(prefAlias, name.replace(/(市|地区|盟)$/, ''), name); // 河池市 → 河池
|
||
}
|
||
|
||
const js = `/**
|
||
* 行政区划离线表(阿里 DataV GeoAtlas 数据,自动生成,请勿手改)
|
||
*
|
||
* 数据源:https://geo.datav.aliyun.com/areas_v3/bound/{adcode}_full.json(免密钥)
|
||
* 层级:国家民政部 adcode 体系(省级 2 位 / 地级 4 位 / 县级 6 位)
|
||
*
|
||
* COUNTY_DV 县级名 → [纬度, 经度, 所属地级名, adcode]
|
||
* PREF_DV 地级简称 → [纬度, 经度, adcode]
|
||
* COUNTY_ALIAS 县别名/简称 → 县全称
|
||
* PREF_ALIAS 地级别名/全称 → 地级简称
|
||
*
|
||
* 生成:node tools/build-admin-dv.cjs C:/tmp/datav-admin.json
|
||
*/
|
||
const COUNTY_DV = ${JSON.stringify(county)};
|
||
const PREF_DV = ${JSON.stringify(prefecture)};
|
||
const COUNTY_ALIAS = ${JSON.stringify(countyAlias)};
|
||
const PREF_ALIAS = ${JSON.stringify(prefAlias)};
|
||
module.exports = { COUNTY_DV, PREF_DV, COUNTY_ALIAS, PREF_ALIAS };
|
||
`;
|
||
|
||
fs.writeFileSync(out, js);
|
||
console.log(`县级 ${Object.keys(county).length},地级 ${Object.keys(prefecture).length},县别名 ${Object.keys(countyAlias).length},地别名 ${Object.keys(prefAlias).length}`);
|
||
console.log('写入', out, (fs.statSync(out).size / 1024).toFixed(0) + 'KB');
|