GIS数据GIS DATA

从村界聚合出乡镇边界:环缝合实测

技术专栏 · 数据处理 · 2026-10-18

手里只有村界,想看乡镇格局——买一份乡镇街道边界包是正路,但还有一个零成本的路子:村界本来就拼得成乡镇。把同一乡镇的村合成一个面,乡镇边界自然浮现。这篇把「溶解」这条流水线在两份真实包上跑通并验证:铜鼓县 97 村溶成 13 个乡镇面,江夏区 456 村溶成 18 个街道面,全部单环、零失败,中间还修出一个「零环」的静默 bug——前后两版数字都摆在这里。

溶解的原理:先消边,再缝合

相邻的两个村共享同一条边界,但走向相反。溶解一群村时,把这些有向边全部登记进一张表:凡是正反两个方向都在表里的边,是群内共享边,互相抵消;剩下的边只面对群外——它们就是溶解后形状的外轮廓,按方向首尾缝合,就得到乡镇面。核心代码二十行:

// 1) 登记:组内全部有向边(端点 6 位小数量化)
for (const f of feats) {
  for (const r of ringsOf(f)) {
    for (let i = 0; i < r.length; i++) {
      const a = r[i], b = r[(i + 1) % r.length];
      if (Q(a[0]) === Q(b[0]) && Q(a[1]) === Q(b[1])) continue;  // 跳过零长边
      edges.set(key(a) + '|' + key(b), [a, b]);
    }
  }
}
// 2) 消去:反向不在表里的边才保留
for (const [k, ab] of edges) {
  if (!edges.has(key(ab[1]) + '|' + key(ab[0]))) leftover.push(ab);
}
// 3) 缝合:按「边的终点 = 下一边的起点」沿方向链接,闭合即成环

有向缝合的好处是无歧义:每条保留边的终点必然是另一条保留边的起点,按起点建倒排表逐环链接即可,不需要任何几何近似。

实测:铜鼓 97 村 → 13 个乡镇面

铜鼓县按村级码前 9 位(乡镇街道代码)分成 13 组,逐组溶解,268 毫秒跑完——13 个组各产出恰好 1 个闭合环,没有一个碎环、零多件。把溶解面填色、村界画成细线叠上去,看看边界是不是长在该在的地方:

铜鼓县 97 个村溶解成 13 个乡镇面的结果图:深色粗线为缝合乡镇边界,白色细线为村界
深色粗线为环缝合出的乡镇边界(13 条全部闭合),白色细线为原村界,面内数字为各村数。

图例里三个「未知 1村」值得说明:它们是茶山、龙门、花山三个林场的村级单元,村名字段没有乡镇前缀(林场不挂乡镇),分组靠村级码正常归组,只是名称派生不出乡镇名。溶解本身不受影响——按码分组对这种「无名」单元照样工作。

规模验证与一个真 bug

换武汉江夏区做规模验证:456 个村按乡镇字段分 18 组,1,650 毫秒跑完,18 个街道全部溶出单环,零多件——江南平原村碎而密,这个量级的溶解依然毫秒级。

但第一版跑出来的是 13 个组全部 0 环:调试打印边表只有 1 条边、内部配对 1、边界 0——97 个村的所有边塌成了一条。根因在取环函数:对 Polygon 类型多包了一层数组,端点变成数组而不是数字,量化后全是 NaN,所有边的键都成了同一个「NaN,NaN|NaN,NaN」,互相抵消到一条。修掉多余的包裹层,13 与 18 个环立刻出来。零环和零命中一样危险:都是静默失败,溶解类脚本跑完先数一数环数,环数对不上组数就要回头查。

口径与边界:派生边界能用到哪

溶解边界是村界的派生物:村界到哪它到哪,继承村界的全部精度与口径。它适合做统计底图、巡查分区、可视化分组;不能冒充官方乡镇勘界——法定边界的取位与程序另有一套。需要官方口径时直接使用乡镇街道边界数据,与村界叠合还能反过来核对村界与乡镇面的套合差。

分组字段优先用村级码前 9 位:它由统计口径维护,不随乡镇改名漂移;乡镇名称字段填充完整的包(如武汉、长沙)也可以直接按字段分组。两份样本分别按码与按字段分组,溶解面数与组数都一一对应。溶解后的乡镇面要参与点位归属、面积统计时,方法与《把1.4万座山峰落到村界:网格索引快了102倍》《村界面积怎么算才准:三种公式的实测差距》完全相同;数据本体在村界数据 · 按市与按区县页按需取用。

常见问题

手里只有村界,怎么得到乡镇边界?
按乡镇字段(或村级码前 9 位)把村分组,组内做溶解:收集组内全部边,成对反向的共享边互相抵消,剩下的边沿方向缝合就成了乡镇外轮廓。本文实测铜鼓县 97 个村溶解成 13 个乡镇面、江夏区 456 个村溶成 18 个街道面,全部单环,分别耗时 268 与 1,650 毫秒。
溶解后出现碎环或零环怎么办?
先查两处。一是坐标取环时的层级:Polygon 的坐标是「环→点」两层,本文第一版多包了一层,端点全变数组、键全成 NaN,所有边塌成一条、一个环都缝不出来且无报错。二是数据本身的缝隙:相邻村对不上边时,边消不掉会留缝。碎环多数是量化精度问题,把量化从 6 位小数放宽到 5 位再试。
溶解出来的乡镇边界和官方勘界一样吗?
不一样,也不应该一样。溶解结果是村界派生边界:村界到哪,乡镇边界就到哪,它继承村界的全部精度与口径,适合做统计底图、巡查分区与可视化。官方乡镇勘界是另一套法定程序。需要法定边界时应使用乡镇街道边界数据,而不是自己溶解。
溶解出现多件面正常吗?
看口径。本文两份样本溶解后全部单环,说明乡镇连片无飞地。若分组里确有飞地村,溶解正确的结果就应该是多件面——件数等于该乡镇的连片区块数。此时多件不是错误,是飞地事实的几何表达,按面件分别使用即可。
按什么字段分组最可靠?
优先用 12 位村级码的前 9 位(乡镇街道代码),它由统计口径维护、不随名称改制漂移;乡镇名称字段填充完整的包也可以直接用。两组数据都实测过:铜鼓按村级码前 9 位分 13 组、江夏按乡镇字段分 18 组,溶解面数与组数一一对应,零多件。