村级GeoJSON怎么挂回行政区划树:跨省村码口径不同的对齐方案
打开一份村镇级边界文件,乡镇字段写着「宁夏回族自治区石嘴山市惠农区惠农」,村级码写着 640205407001。这个乡镇名在权威乡级目录里查不到,因为惠农区底下只有育才路、南街、中街、北街、河滨街、火车站六个街道和红果子、尾闸、园艺三个镇,外加庙台、礼和、燕子墩三个乡。640205407 是一个已经撤销的乡级单位,边界还在,代码早就不在现行区划树上了。
全国村级geojson数据下载撞上这类问题几乎是必然:文件能下、能画、能算面积,可一旦想按乡镇汇总、想把它接进省市区县乡镇四级树,就会发现村一级是整个行政区划数据里口径最乱的一层。下面这份对齐方案,实测对象是 30 余份省级封装的村级边界文件。
村级原生属性其实只有三套口径
把各省村镇级边界文件放在一起比较,属性结构是三种不同的东西,不是同一种东西的残缺版本。
第一种带完整 12 位村级码,省市区县乡镇字段也齐全,四川、山东、河南、云南、内蒙、吉林、宁夏都属于这一类。第二种有规范的省市区县乡镇名称,但村级码整列为空,天津、上海、湖北是典型。第三种连乡镇字段都不给,只剩区县一级,上海的文件甚至乡镇字段整列为空串。至于中国台湾那份,类型标记写的是「台湾村里」,和大陆的行政村、居委会不是一套分类,需要单独处理。
| 省级样本 | 要素数 | 村级码缺失 | 12 位码挂得上乡镇 | 主要口径 |
|---|---|---|---|---|
| 宁夏 | 2,947 | 1(0.0%) | 2,810 / 2,946(95.4%) | 完整码 |
| 四川 | 41,297 | 0(0.0%) | 38,026 / 41,297(92.1%) | 完整码 |
| 吉林 | 10,194 | 0(0.0%) | 7,350 / 10,194(72.1%) | 完整码 |
| 内蒙 | 14,570 | 0(0.0%) | 9,501 / 14,570(65.2%) | 完整码 |
| 山东 | 85,879 | 0(0.0%) | 49,298 / 85,879(57.4%) | 完整码 |
| 河南 | 50,371 | 0(0.0%) | 27,472 / 50,371(54.5%) | 完整码 |
| 北京 | 8,021 | 0(0.0%) | 4,134 / 8,021(51.5%) | 完整码 |
| 安徽 | 17,848 | 2(0.0%) | 2,001 / 17,846(11.2%) | 完整码 |
| 天津 | 5,847 | 5,847(100%) | 无从挂载 | 仅名称 |
| 上海 | 5,601 | 5,601(100%) | 无从挂载 | 仅区县 |
这张表最重要的信息不在缺失率,而在最后一列。带码的省份里,即使码是完整 12 位,直接拿前 9 位去现行乡级目录里查,命中率从 11.2% 到 95.4% 横跨整个区间。安徽只有 11.2% 能挂上,说明这份文件里的乡级码和现行乡级目录几乎对不上号——问题不在文件本身,它采用的是更早的编码世代。
村级码末三位能反推村居委会属性
12 位村级码的结构是前 6 位县级码、第 7 到 9 位乡级码、最后 3 位村级顺序码。多数人只关心前 9 位,其实最后 3 位有明确规律:001 到 199 是居民委员会段,200 起是村民委员会段。
以宁夏 2,946 条有效码统计,落在居民委员会段的只有 111 条,村民委员会段 2,835 条。这个比例和宁夏的城市化水平是对得上的。反过来说,一份村镇级文件如果类型字段整列写着「行政村」,但末三位大量落在 0xx,那它实际混装了大量社区,按村汇总人口或做村级统计时口径就偏了。判断一条记录该归哪一类,看末三位比看名称里有没有「社区」两个字靠谱。
挂载失败的记录,用「区县 + 乡镇名」做复合键兜底
天津蓟州区 5,847 条记录村级码整列为空,但省、区县、乡镇三级名称都在。这类数据的挂载路线只能走名称匹配,而直接用乡镇名做键会出事:天津全市 273 个乡镇名里有 4 个跨区县重复,开发区、上杭路街道、春华街道、唐家口街道各出现了不止一次。
修法是复合键加代码反查,先拿区县名定位县级码,再用县码前缀加乡镇名反查 9 位乡级码:
// 输入村界要素的 properties,输出补全后的四级码
// countyIndex: 县级名 -> 6 位县码;townIndex: 6 位县码 -> Map<乡镇名, 9 位乡级码>
function rekey(props, countyIndex, townIndex) {
const ct = countyIndex.get(props["区县"]);
if (!ct) return null; // 区县名都对不上,直接放弃不猜
const raw = String(props["村级码"] || "");
if (raw.length === 12) {
const town9 = raw.slice(0, 9);
// 有码也别盲信:乡级码得能在现行目录里查到,再说乡镇字段是否一致
if (townIndex.get(ct)?.has(props["乡镇"])) {
return { town: town9, village: raw, via: "code" };
}
}
// 无码或码失效,落到名称匹配;同名乡镇靠县码前缀消歧
const town9 = townIndex.get(ct)?.get(props["乡镇"]);
if (!town9) return null; // 匹配不上就留空,不硬塞父级
// 村码缺失时用乡级码 + 占位段,保证层级链条完整但不冒充真实村码
return { town: town9, village: "", via: "name" };
}
via 字段是刻意留的。它标记这条记录是按码挂上的还是按名挂上的,后者将来遇到区划调整需要重挂,重挂时要优先处理。不标的话,两种来源的数据混在一张表里,出问题无从回溯。
挂不上的记录宁可留空也不要补一个「最近似」的父级。村级数据一旦被塞进错误的乡镇,按乡镇汇总的上报口径会整体偏掉,比留空难查得多。留空至少在下游一眼看得出来。
落地时的取舍
一个区县的村级边界,三十几万条记录里能自动挂到现行乡级树的通常在六成到九成之间,剩下的必须人工或半自动复核。如果只做展示用途——按村镇看边界形状、按区县统计村的数量——挂载精度到区县一级就够,不必强行下探到乡镇。
需要逐村汇总的场景另想办法。一种做法是把挂载结果按 via 分成两条流水线,按码挂上的直接入库,按名挂上的先跑一遍同名冲突检测再入。另一种是先把省级村界按区县切片,逐个区县核对,把复核工作量摊到可接受的粒度。两类需求都绕不开全国省市区县区划边界数据下载之后的第一道工序,也就是把各层级文件的口径先对齐再入库。
如果手上的源文件需要先做跨格式转换或压缩瘦身,试过几轮之后会发现瓶颈常常不在转换工具,而在源数据的全国省市区县geojson数据下载环节有没有把层级口径统一。本系列讲格式选型的那几篇可以直接拿来用;转换前后各核一次校验和,能省掉不少返工。
可视化交付环节有个细节值得提前处理。做汇报材料时,区块复杂度高的村镇边界直接塞进演示文稿往往卡顿,可编辑的矢量底图更适合这个场景。拿单个省或市做汇报底图,可以到 地图 PPT 模板选购页 按省、市逐级下钻挑一份,先预览图层再决定下载哪一档,比自己从 GeoJSON 现场渲染省事。
村一级数据的价值在于它是最细的地理单元,能把乡镇级汇总里被平均掉的信息还原出来。代价是它也是口径最不统一的一层——同一批数据里可能同时存在现行码、历史码和无码三种状态。GeoJSON下载环节拿到手的原始封装保留这种差异并不算缺陷,硬把它抹平才会在后面对不上账。本文涉及的地图数据仅用于技术讨论,边界与区划口径以国家有关部门发布的标准为准。