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