边界数据拓扑检查:一段脚本查出缝隙与重叠
技术专栏 · 数据处理 · 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 个村)。结果表:
| 样本 | 村级面 | 坐标点 | 内部共享边 | 外轮廓边 | 同向重复 | 自相交 |
|---|---|---|---|---|---|---|
| 铜鼓县 | 97 | 125,191 | 60,055 对 | 4,892 | 28 键 / 58 边 | 0 |
| 宜丰县 | 354 | 245,945 | 117,744 对 | 10,096 | 0 | 0 |
| 重庆全市 | 10,847 | 1,502,113 | 731,837 对 | 26,743 | 17 键 / 34 边 | 未测* |
*自相交为逐环暴力检查,只在两个县包上跑了全量;重庆包跳过该项。三份数据 95%–98% 的边都配成了内部共享边,外轮廓边集中在县域边缘——形状与行政区划常识完全吻合。
脚本本身也得先过体检才能信。做一次阳性对照:从铜鼓包里删掉棋坪镇大梅村(环边 1,312 条)再跑——未配对边从 4,892 涨到 5,775。这笔账能对平:该村 1,097 条与邻村共享的边失去伙伴,自身 215 条县界边同时移出统计,净增 882,多出的 1 条来自样本里既有的重复边。删一个村,缝隙当场现形。
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 位小数)取整再配对;取整后仍配不上的边才是真问题。