12 位统计用区划代码怎么拆成四级行政区划树
全国行政区划 GeoJSON 的 properties 里,adcode 是主键。可 6 位代码和 12 位代码是两套东西,混着用,两边的表就是对不上。
两套代码的关系
GB/T 2260 规定的行政区划代码是 6 位数字,前 2 位表示省级,中间 2 位表示地级,后 2 位表示县级。国家统计局另外发布一套统计用区划代码,长度是 12 位:前 6 位与行政区划代码保持一致,第 7 到 9 位表示街道或乡镇,第 10 到 12 位表示居委会或村委会。
统计局的村级清单里还有一个独立的 3 位城乡分类代码,例如 111 表示主城区、220 表示村庄。它和区划代码是并列的两个字段,不是区划代码的一部分,别拼到后面去。
| 代码 | 位数 | 覆盖层级 | 来源 | 更新频率 |
|---|---|---|---|---|
| 行政区划代码 | 6 | 省 / 地 / 县 | GB/T 2260 | 有区划调整时变 |
| 统计用区划代码(乡级) | 9 | 增加乡镇街道 | 国家统计局 | 每年一版 |
| 统计用区划代码(村级) | 12 | 增加居委会村委会 | 国家统计局 | 每年一版 |
从 6 位代码判定层级
判定规则全看尾部的 00,不用查表。
function levelOf(code) {
if (!/^\d{6}$/.test(code)) return null;
if (code.slice(2) === '0000') return 'province'; // 后 4 位为 0,省级
if (code.slice(4) === '00') return 'city'; // 后 2 位为 0,地级
return 'district'; // 县级
}
县级那 2 位还有一套沿用已久的分段约定:01 到 20 给市辖区,21 到 80 给县和自治县、旗,81 到 99 给县级市。这是编码惯例,不是强制约束。拿它做粗分类没问题,当成判据去过滤数据就会出错。
构树
拿到 12 位清单以后,父子关系全部由前缀推导,不需要额外的 parent 字段。
// rows: [{ code: '110105001001', name: '××居委会' }, ...] 12 位原始清单
function buildTree(rows) {
const cut = [2, 4, 6, 9, 12]; // 省 / 市 / 县 / 乡 / 村 的前缀长度
const pad = ['0000', '00', '', '', '']; // 补齐到 6 位
const index = new Map();
for (const row of rows) {
let parent = null;
for (let i = 0; i < cut.length; i++) {
const code = row.code.slice(0, cut[i]) + pad[i];
let node = index.get(code);
if (!node) {
node = { code, level: i, name: '', children: [], seen: new Set() };
index.set(code, node);
}
if (parent && !parent.seen.has(code)) {
parent.seen.add(code);
parent.children.push(node);
}
parent = node;
}
parent.name = row.name;
}
return [...index.values()].filter(n => n.level === 0);
}
seen 那个 Set 别省。村级清单几十万行,改用 children.includes 去重,复杂度直接退化成平方级。
跟 GeoJSON 对齐
区县面按 adcode 前缀上卷到地市,是最常用的一步:
const groups = new Map();
for (const f of fc.features) {
const adcode = String(f.properties.adcode);
const cityCode = adcode.slice(0, 4) + '00';
if (!groups.has(cityCode)) groups.set(cityCode, []);
groups.get(cityCode).push(f);
}
分完组再做几何合并,具体做法在 边界上卷合并 里讲过。把统计数据 join 到面上着色,可以看 分级设色地图。
层级完整性顺手校验一遍,把没有上级的孤儿节点挑出来:
const known = new Set(fc.features.map(f => String(f.properties.adcode)));
const orphans = [...known].filter(
c => c.slice(4) !== '00' && !known.has(c.slice(0, 4) + '00')
);
三个坑
前缀上卷不是到处都成立。省直辖县级行政区的地级位固定为 90,河南济源是 419001,湖北仙桃是 429004,神农架林区是 429021,它们的 4190 和 4290 只是一个占位分组,现实中并没有对应的地级市实体。直辖市也一样,北京是 110000,下面挂着一个 110100 市辖区,再往下才是 110105 朝阳区,这个 110100 在 GeoJSON 里通常没有独立几何。上卷脚本得把这类虚拟层单独拎出来处理,不然会凭空造出一批空壳面。
adcode 必须按字符串存。12 位代码小于 2 的 53 次方,也就是 9007199254740991,JavaScript 的 Number 表示得下,精度不丢。真正出事的是 Excel:单元格宽度不够时它显示成 1.10105E+11,另存为 CSV 就按显示格式写出去,代码当场损坏。字段定成字符串,这一整类问题就绕开了,slice 取前缀也更顺手。属性字段的处理链路在 Shapefile 转 GeoJSON 实战 里更完整。
代码是带版本的。统计用区划代码每年发布一版,标准时点是当年 10 月 31 日,撤县设区之类的调整会让代码从 21 到 80 段跳到 01 到 20 段。拿 2020 年的边界去 join 2024 年的统计表,肯定漏一批,而且漏得毫无声息。我的做法是在 properties 里记一个版本年份,join 之前先比对两边年份对不对得上。字段命名相关的规范问题,RFC 7946 的隐蔽坑 里也提过。
小结
adcode 不是一串普通字符,它是一棵编码进字符串里的树。前缀结构吃透了,四级联动、层级上卷、跨表 join 都不用另建关系表,一个 slice 就够。全国四级行政区划的 GeoJSON 数据可以在 geojsoncn.com 获取,2800 多个区县的 adcode 字段均按字符串存储。