#!/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'); })();