用 GeoJSON 比对两个版本的行政区划:拆分、合并、更名一眼看穿
民政部每年都会动一次行政区划代码,区县、乡镇之间撤并、拆分、改名是常事。手头有两份不同年份的行政区划 GeoJSON,想一眼看出到底变了哪些,靠肉眼翻名字不现实。
办法其实很直接:拿行政区划代码(adcode)当稳定主键,把两份 FeatureCollection 各自建字典,再分类比对。
一、为什么用 adcode 而不是名字
区县会改名(典型的就是撤县设区),乡镇名字改得更勤。但 6 位或 12 位 adcode 在民政部体系里相当于身份证号,12 位统计用区划代码 的前缀结构能稳定定位省、市、区县、乡镇四级。把 adcode 当主键,改名和拆分这两类变化才分得清。
二、建字典 + 三分类
两份数据读进来,按 adcode 建 Map:
import { area } from '@turf/turf';
function toMap(fc) {
const m = new Map();
for (const f of fc.features) {
// adcode 必须当字符串存,110101 不能变成数字
m.set(String(f.properties.adcode), f);
}
return m;
}
const oldMap = toMap(oldFC);
const newMap = toMap(newFC);
const added = [], removed = [], renamed = [], areaChanged = [];
for (const [code, f] of newMap) {
if (!oldMap.has(code)) { added.push(code); continue; }
const o = oldMap.get(code);
if (o.properties.name !== f.properties.name) renamed.push(code);
const r = area(f) / area(o);
if (r < 0.92 || r > 1.08) areaChanged.push({ code, ratio: +r.toFixed(3) });
}
for (const [code] of oldMap) if (!newMap.has(code)) removed.push(code);
added 多半对应"一分为二":原单位被拆,新 adcode 凭空出现。 removed 则是被合并掉的那一方。只看 adcode 集合的对称差,拆和并的轮廓就出来了。
三、用面积比识别边界微调
同一个 adcode 两边都在,但 area() 返回的球面面积(平方米)比值跟 1 差太远,多半是边界被切了一刀或并了一块。我习惯留 8% 容差(0.92~1.08),把投影噪声和轻微修测挡在外面。面积必须走 turf 的测地线算法,别拿 shapely 在平面经纬度上算,那出来的是毫无意义的平方度,坐标系声明 这步本就钉在 ETL 链路里。
四、结果落地成一张表
| 变化类型 | adcode 表现 | 典型成因 |
|---|---|---|
| 新增 added | 仅新表有 | 拆分产生新乡镇 |
| 消失 removed | 仅旧表有 | 被并入相邻单位 |
| 更名 renamed | 两边都有、name 不同 | 撤乡设镇、改字 |
| 面积突变 areaChanged | 两边都有、面积比越界 | 边界微调 / 飞地划出 |
这张表导出成 CSV,再接上 按 adcode 前缀上卷地市省级边界 的 dissolve 思路,宏观上就能看出某一年是不是把两个县并成了一个区。
五、三个容易踩的坑
1. adcode 当数字会翻车。 110101 看着没变,可一旦被 JSON 解析成 Number 又转回字符串,前导零和超大数精度都可能出错。老老实实存字符串,比较也走字符串。
2. 更名常带着 adcode 一起变。 撤县设区往往发新代码(比如某县 410XXX 变市辖区 4109XX),这时它在新表里是 added、旧表里是 removed,对称差会误判成"删了又建"。要认出真更名,得补名称相似度或人工核对。
3. 坐标系要对齐。 两份 GeoJSON 都得是 WGS84。一份 GCJ-02 一份 WGS84 直接比面积,差值里掺着加偏位移,抽稀前的几何归一化 也兜不住。比对前先把坐标系统一。
流程固化成脚本,每次民政部更新代码后跑一遍,行政区县的"家谱变动"自己就浮出来了。更多格式与坐标系细节见 主站。