GIS数据GIS DATA

边界数据拓扑检查:一段脚本查出缝隙与重叠

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

本站的村界商品页写着「面与面之间无缝隙、无重叠」。这句话是批次清洗的目标,但空口无凭——这篇就把交付的村界包当被检对象,用二十几行的有向边配对脚本真查一遍:三份数据、一次删村的阳性对照、连同查出来的 0.02% 重复边长什么样,全部摆在明面上。检查方法比结论更有用:换成任何来源的边界数据,这套流程照跑。

三类问题,各有各的破坏力

边界数据的拓扑问题常说的有三种。缝隙:相邻面之间留下没人认领的窄条,按空间关系挂接的业务数据掉进缝里,统计漏算;重叠:同一段地面被两个面各盖一次,面积、产值、人口汇总全部翻倍计;自相交:一个面的边界自己穿过自己,几何不合法,多数空间函数会直接报错或给出荒谬结果。缩略图上肉眼几乎无法分辨前两种——一条 0.5 米宽的缝在市级图上不到一个像素,进了报表才发作。

有向边配对法:让相邻面自己互相作证

思路是给每个面环的每条边定向。相邻的两个面共享同一条地面边界,但走向必然相反——一个沿它顺时针走,另一个就逆时针走。把全部边按坐标量化后登记进一张表,逐条找它的反向伙伴:

const Q = v => Math.round(v * 1e6) / 1e6;      // 6 位小数量化
const edges = new Map();                        // 有向边 -> 出现次数
for (const f of feats) {
  const ring = f.geometry.coordinates[0];
  for (let i = 0; i < ring.length; i++) {
    const a = ring[i], b = ring[(i + 1) % ring.length];
    const key = Q(a[0]) + ',' + Q(a[1]) + '|' + Q(b[0]) + ',' + Q(b[1]);
    edges.set(key, (edges.get(key) || 0) + 1);
  }
}
let interior = 0, boundary = 0, dup = 0;
for (const [key, c] of edges) {
  if (c > 1) dup++;                             // 同向重复 = 重叠扫过
  const [ab, cd] = key.split('|');
  const rev = edges.get(cd + '|' + ab) || 0;
  if (c === 1 && rev === 1 && key < cd + '|' + ab) interior++;
  if (c === 1 && rev === 0) boundary++;         // 外轮廓(或缝隙)
}

三种结局一一对应三类信息:interior 是配对成功的内部共享边——面片拼合严丝合缝的证据;boundary 是没有反向伙伴的边——在县界、海岸这类位置合法,在县域内部成片出现就是缝隙;dup 是同向重复——正常拼合的面上不该出现,出现了就是重叠扫过。前提只有一个:相邻面共享同一批顶点。同一批次清洗过的数据直接成立,来源混杂的数据先按固定网格取整再配。

实测三份数据,再用删村验证方法本身

跑三个样本:铜鼓县(97 个村)、宜丰县(354 个村)、重庆全市(10,847 个村)。结果表:

样本村级面坐标点内部共享边外轮廓边同向重复自相交
铜鼓县97125,19160,055 对4,89228 键 / 58 边0
宜丰县354245,945117,744 对10,09600
重庆全市10,8471,502,113731,837 对26,74317 键 / 34 边未测*

*自相交为逐环暴力检查,只在两个县包上跑了全量;重庆包跳过该项。三份数据 95%–98% 的边都配成了内部共享边,外轮廓边集中在县域边缘——形状与行政区划常识完全吻合。

脚本本身也得先过体检才能信。做一次阳性对照:从铜鼓包里删掉棋坪镇大梅村(环边 1,312 条)再跑——未配对边从 4,892 涨到 5,775。这笔账能对平:该村 1,097 条与邻村共享的边失去伙伴,自身 215 条县界边同时移出统计,净增 882,多出的 1 条来自样本里既有的重复边。删一个村,缝隙当场现形。

铜鼓县棋坪镇 5 个相邻村的共享边实测图,橙色为配对成功的内部边
铜鼓县棋坪镇 5 个相邻村:橙色是配对成功的共享边(1,072 段,脚本算出),深灰是各自的外轮廓。柏树村经窄颈与棋坪村相接,是村级边界里典型的带飞地形态。

0.02% 的重复边是什么

铜鼓的 28 个重复键值得拆开看:把每条重复有向边的归属面挖出来,16 处跨村、12 处同村。跨村的(如港口乡港口村与赤洲村)是相邻两村在极短的共享段上朝同一方向各扫了一遍——微小的局部重叠;同村的(柏树村一处环上出现 6 次)是带飞地的村环在接触点上自我接触,属无害形态。全部重复边占铜鼓 125,064 条边的 0.02%,重庆包更低到 0.001%。量级意味着制图与常规统计感知不到,但精密面积计算前应当知道它们在、测得出、能定位。

落地清单:拿到边界数据先跑这四步

第一,量化配对(上文脚本),记录 interior / boundary / dup 三个数;第二,看 boundary 的分布——聚在研究区外缘是正常外轮廓,散布在内部就是缝隙;第三,对 dup 逐条挖归属面,区分跨面重叠与同面自接触;第四,删一个面做阳性对照,确认检查器能咬住已知问题,再相信它报出的其他数字。数据体量与精度的关系在《GeoJSON 太大?先抽点,再谈小数位》里有完整实测;要验证的对象——按市、按县交付的村界包在村界数据 · 按市与按区县页,本文的铜鼓、宜丰两包即出自村界产品线(背景见《村界数据下载:行政村边界矢量数据的获取路径》);裁出目标区域再查的手法见《从全国边界数据中提取指定省市区县:三种做法》。

常见问题

边界数据的缝隙和重叠有什么危害?
缝隙让落在缝里的要素没有归属,按空间连接统计会漏算;重叠让同一段地面被两个面各算一次,面积和产值汇总会翻倍。肉眼在缩略图上几乎看不出这两类问题,等发现时往往已经进了报表,所以拿到数据先做拓扑检查再使用。
不装 GIS 软件,怎么检查边界数据的拓扑?
用有向边配对法:把每个面环的边按坐标量化成有向边,相邻面必然沿同一条边反向而行。存在反向配对的边是内部共享边,没有反向的边是外轮廓,同一条有向边被扫过两次就是重叠信号。本文的脚本二十余行,Node 环境直接运行,铜鼓县 97 个村一秒内出结果。
有向边配对法可靠吗?怎么验证?
做阳性对照:从铜鼓县包里删掉一个村(棋坪镇大梅村,环边 1,312 条)再跑一遍,未配对边立刻从 4,892 条涨到 5,775 条,与净账 882(1,097 条共享边失去伙伴、215 条县界边移出)只差 1 条既有重复边,方法被验证有效。任何检查脚本都建议这样先证明它能抓住问题,再相信它报出的零。
检查出的重复边都是数据缺陷吗?
要分两类看。跨村同向重复是真实存在的微重叠,本文铜鼓样本有 16 处;同村重复多是带飞地的村环在接触点上自我接触,属无害形态,铜鼓样本 12 处。铜鼓全部重复边占边总数的 0.02%,制图与常规统计感知不到,但做精密面积计算前应当知晓并处理。
相邻面的顶点对不上,配对法还有效吗?
配对的前提是相邻面共享同一批顶点。同一批次、经过统一拓扑清洗的数据可以直接配对,本文三份样本的配对率在 95% 到 98% 之间。来源混杂的数据常出现毫厘级顶点错位,此时先把坐标按固定网格(如 6 位小数)取整再配对;取整后仍配不上的边才是真问题。