用阿里 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)
118 lines
4.0 KiB
JavaScript
118 lines
4.0 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* 拉取全国行政区划边界(阿里 DataV GeoAtlas,免密钥)。
|
||
*
|
||
* 为什么不用 Overpass / OSM:
|
||
* 1) Overpass 公共实例限流极狠,实测连续大查询后连 1KB 小查询都 502/504;
|
||
* 2) OSM 中国的 admin_level 层级与直觉相反、归属字段不可靠;
|
||
* 3) DataV 用的是国家民政部 adcode 体系,层级清晰(省 → 市 → 县),
|
||
* 且**自带县级边界多边形**,能做精确点面判定。
|
||
*
|
||
* 流程:100000_full.json 拿省 → 省文件拿地级市 → 市文件拿县。
|
||
* 直辖市 / 省直辖县级单位由省文件直接给出(level != 'city')。
|
||
*
|
||
* 用法:node tools/fetch-datav-admin.cjs C:/tmp/datav-admin.json
|
||
*/
|
||
const fs = require('fs');
|
||
|
||
const BASE = 'https://geo.datav.aliyun.com/areas_v3/bound';
|
||
const UA = { 'User-Agent': 'Mozilla/5.0 (TripSense build script)' };
|
||
|
||
async function getJSON(adcode) {
|
||
const url = `${BASE}/${adcode}_full.json`;
|
||
for (let attempt = 0; attempt < 4; attempt++) {
|
||
try {
|
||
const res = await fetch(url, { headers: UA });
|
||
if (!res.ok) throw new Error(`${res.status}`);
|
||
const text = await res.text();
|
||
return JSON.parse(text);
|
||
} catch (e) {
|
||
if (attempt === 3) throw new Error(`${adcode}: ${e.message}`);
|
||
await new Promise((r) => setTimeout(r, 800 * (attempt + 1)));
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function countPts(coords) {
|
||
let n = 0;
|
||
const walk = (c) => {
|
||
if (typeof c[0] === 'number') { n++; return; }
|
||
for (const x of c) walk(x);
|
||
};
|
||
walk(coords);
|
||
return n;
|
||
}
|
||
|
||
async function mapLimit(list, limit, fn) {
|
||
const out = [];
|
||
let idx = 0;
|
||
const workers = Array.from({ length: limit }, async () => {
|
||
while (idx < list.length) {
|
||
const i = idx++;
|
||
try { out[i] = await fn(list[i], i); } catch (e) { out[i] = null; console.error(' !', list[i], e.message); }
|
||
}
|
||
});
|
||
await Promise.all(workers);
|
||
return out;
|
||
}
|
||
|
||
function pack(f, extra) {
|
||
const g = f.geometry || { type: '', coordinates: [] };
|
||
return {
|
||
adcode: String(f.properties.adcode),
|
||
name: f.properties.name,
|
||
level: f.properties.level,
|
||
centroid: f.properties.centroid || f.properties.center || null,
|
||
center: f.properties.center || null,
|
||
geomType: g.type,
|
||
coordinates: g.coordinates,
|
||
pts: countPts(g.coordinates),
|
||
...extra,
|
||
};
|
||
}
|
||
|
||
(async () => {
|
||
const outFile = process.argv[2] || 'C:/tmp/datav-admin.json';
|
||
console.error('拉取省级…');
|
||
const provDoc = await getJSON('100000');
|
||
const provinces = provDoc.features.map((f) => ({
|
||
adcode: String(f.properties.adcode),
|
||
name: f.properties.name,
|
||
level: f.properties.level,
|
||
}));
|
||
console.error(`省 ${provinces.length} 个`);
|
||
|
||
// 第一层:每个省的直接下级(地级市,或直辖市的区)
|
||
console.error('拉取省市层…');
|
||
const firstLevel = await mapLimit(provinces, 5, async (p) => {
|
||
const doc = await getJSON(p.adcode);
|
||
return (doc.features || []).map((f) => pack(f, {
|
||
province: p.name,
|
||
provinceAdcode: p.adcode,
|
||
childrenNum: Number(f.properties.childrenNum || 0),
|
||
}));
|
||
});
|
||
|
||
const cities = firstLevel.flat().filter(Boolean);
|
||
const direct = cities.filter((c) => c.level !== 'city');
|
||
const cityList = cities.filter((c) => c.level === 'city' && c.childrenNum > 0);
|
||
console.error(`地级市 ${cityList.length} 个,直辖县级 ${direct.length} 个,拉县级…`);
|
||
|
||
const countyDocs = await mapLimit(cityList, 6, async (c) => {
|
||
const doc = await getJSON(c.adcode);
|
||
return (doc.features || []).map((f) => pack(f, {
|
||
province: c.province,
|
||
provinceAdcode: c.provinceAdcode,
|
||
city: c.name,
|
||
cityAdcode: c.adcode,
|
||
}));
|
||
});
|
||
|
||
const counties = countyDocs.flat().filter(Boolean);
|
||
console.error(`县级 ${counties.length} 个,总顶点 ${counties.reduce((s, c) => s + c.pts, 0)}`);
|
||
|
||
fs.writeFileSync(outFile, JSON.stringify({ provinces, cities, counties }));
|
||
console.error('写入', outFile, (fs.statSync(outFile).size / 1024 / 1024).toFixed(1) + 'MB');
|
||
})();
|