把村界拼成县界:两套数据套合校验实测
技术专栏 · 数据处理 · 2026-10-28
站内有两套互相独立的边界:一套是村界包,7,404 个村面管到临沂每个行政村;另一套是全球线,中国区第 4 级给了 2,891 个区县面,顶点稀疏、面向宏观底图。同一个沂水县,一边是 1,063 个村面溶解出的精细轮廓,一边是只有 198 个顶点的简化线——把前者拼成县界,跟后者叠在一起,到底差多少?本文逐县量给你:12 个县的中位缝隙 119 到 220 米,山区县全段吻合,个别城区藏着公里级的离群段。
先看周长:简化线偷偷切了多少角
套合之前,周长先暴露了差距。把每个县的村面按归属溶解成轮廓(有向边配对法,见《从村界聚合出乡镇边界:环缝合实测》),与全球线的县界周长两两相除:
| 县 | 村界周长 | level4 周长 | 比值 | 中位缝隙 | ≤500 米占比 |
|---|---|---|---|---|---|
| 沂水县 | 613km | 341km | 1.80 | 134.6m | 100% |
| 兰陵县 | 402km | 241km | 1.67 | 125.1m | 91.9% |
| 平邑县 | 386km | 222km | 1.74 | 124.9m | 100% |
| 莒南县 | 410km | 221km | 1.86 | 131.7m | 96.0% |
| 郯城县 | 345km | 195km | 1.77 | 173.8m | 87.1% |
| 沂南县 | 427km | 243km | 1.76 | 129.5m | 97.2% |
| 费县 | 448km | 237km | 1.89 | 119.0m | 95.9% |
| 蒙阴县 | 375km | 230km | 1.63 | 133.8m | 100% |
| 兰山区 | 186km | 150km | 1.24 | 142.4m | 93.9% |
| 河东区 | 174km | 167km | 1.04 | 202.3m | 78.3% |
| 临沭县 | 308km | 173km | 1.78 | 161.2m | 91.0% |
| 罗庄区 | 140km | 128km | 1.09 | 220.0m | 60.9% |
乡村县的比值清一色在 1.6 到 1.9 之间:沂水县 613 公里的村界轮廓,被简化线用 341 公里就画完了。粗算折点密度更直白——341 公里摊在 198 个顶点上,平均每 1.7 公里才拐一个弯,村界里那些贴着山脊、绕着耕地的小弯全被拉成了弦。城区反过来:罗庄、河东的比值只有 1.04 到 1.09,城区边界本来就横平竖直,简化省不出多少。
缝隙账单:山里贴得死,城里藏着离群段
周长只给总量,缝隙要看分布。把全球线县界的全部顶点逐个投到村界轮廓上,量到最近边界段的距离(网格索引加速,12 县约 1,400 个顶点几秒跑完)。结果分成了泾渭分明的两群:
九个县贴得很好。中位缝隙全部落在 119 到 174 米之间;沂水、平邑、蒙阴三个山区县最漂亮——全部顶点都在 500 米以内,最大偏差分别只有 418、393、418 米。这说明沂蒙山区的县界基本就是沿村界走的,两套数据在山里高度一致。
三个城区县藏着离群段。罗庄区 60.9% 的顶点落在 500 米内,p90 冲到 3.9 公里;河东 p90 3.2 公里;离群最远的顶点普遍在 4 到 4.8 公里(罗庄、河东、兰山、兰陵、费县、沂南都有)。这些不是随机噪声,是成段的偏离——城区的区划调整频繁(开发区代管、街道重划),两套数据对「这段线在哪」的口径出现了公里级分歧。中位数看全局,离群段看局部,两眼都要睁。
套合校验脚本:任何两套边界都这么量
整套校验三步,二十来行。第一步把细的一方溶解成轮廓(用《从村界聚合出乡镇边界》的无向配对净流量法,北京篇也用过);第二步给粗的一方建网格索引;第三步逐顶点求到最近线段的距离,出中位数和分位数:
// lv4:全球线县界顶点数组;grid:村界轮廓边的 0.005° 网格索引
function nearest(grid, px, py) { // 点到最近线段距离(米)
let best = Infinity;
for (let k = 1; k <= 24; k++) { // 扩环搜索,找不到就说明远离目标
for (let di = -k; di <= k; di++) for (let dj = -k; dj <= k; dj++) {
if (Math.max(Math.abs(di), Math.abs(dj)) !== k) continue;
for (const e of grid.get((cell(px) + di) + ':' + (cell(py) + dj)) || []) {
const d = distToSegMeters(px, py, e);
if (d < best) best = d;
}
}
if (best < Infinity && k >= 2) break;
}
return best;
}
const gaps = lv4Verts.map(([x, y]) => nearest(grid, x, y)).filter(d => d < 5000).sort((a, b) => a - b);
// 中位数 = gaps[gaps.length >> 1];p95 = gaps[Math.floor(gaps.length * 0.95)]
它跟《边界数据拓扑检查:一段脚本查出缝隙与重叠》是一对兄弟:拓扑检查问「一套数据内部自洽吗」,套合校验问「两套数据互相咬合吗」。《合并京津村界数据:13,868面一条缝》量的两市接缝,用的也是这套点到线段的底子。
两条使用结论
其一,全球线的区县界是底图,不是参照系。它把沂水县画成 198 个顶点是有意为之——2,891 个县面铺满全国还得轻,简化是必要代价;拿它做省市级示意、路径配图、国际报表都合适,拿它当村级分析的基准线就会把 200 米级的误差当真。
其二,县界与村界混用的流程,先跑一遍套合校验再定容差。多数县 500 米容差能覆盖九成以上顶点,罗庄这样的城区要把离群段单独拎出来看——可能一侧口径更新、一侧滞后。想让两套数据在自己的项目里互相对齐,村界包在「村界数据 · 按市」按市取,全球线的区县界在「全球行政区划边界」按国家与层级取;需要正式的区县边界包则看「省 / 市 / 区县边界包」。
常见问题
- 村界拼起来的县界,能当正式县界用吗?
- 形状可以,精度是两码事。村界溶解出的县轮廓与全球线的区县界逐县实测:中位缝隙 120 到 220 米,山区县全段吻合,城区存在公里级离群段。做市域示意图没问题,做辖区归属判定要用正式的区县边界数据,容差至少留 500 米。
- 全球线的县界为什么这么粗?
- 全球数据面向宏观场景做了简化:沂水县的 level4 县界只有 198 个顶点,周长 341 公里,平均每 1.7 公里才一个折点;而同县 1,063 个村面溶解出的轮廓周长 613 公里,是简化线的 1.8 倍。简化线画国省道级底图正合适,套到村级尺度就会露馅。
- 两套边界对不上,是哪套数据错了?
- 都不是错。两套数据独立整理、面向的尺度不同:村界是管理边界的精细表达,全球线是跨 252 国统一口径的简化底图。实测差异有其规律——九个县的中位缝隙 119 到 174 米、500 米内顶点占比 87% 到 100%;三个城区县出现成段的离群顶点(最远 4 到 5 公里),对应区划调整与简化取舍的段落,属于各自口径的如实表达。
- 做叠加分析时容差该留多少?
- 本文的口径:村级分析不要拿简化县界当参照,直接用村界自身的归属字段;县界与村界混用时,缓冲容差至少 500 米起,覆盖全部 12 县的中位与 95 分位缝隙;涉及罗庄、河东这类存在离群段的城区,先跑一遍本文的套合脚本看离群分布,再定容差。
- 乡镇界和村界的套合也能这样校验吗?
- 方法完全通用:把参与的一方溶解成目标轮廓,另一方建网格索引,逐顶点算到最近线段的距离,看中位数和分位数。把本文脚本里的村界包换成乡镇街道边界包、区县界换成市界即可,缝隙量级会随两侧数据的精度组合变化,脚本一行不用改。
- 自己复现这个校验需要哪些数据?
- 两份任意来源的边界数据加一个 Node 环境即可:溶解用站内《从村界聚合出乡镇边界》的有向边配对法,缝隙测量用本文的网格加最近线段脚本,全流程在 7,404 个村面的临沂包上约 15 秒跑完。村界包在「村界数据 · 按市」页取,全球线的区县界在「全球行政区划边界」页按国家与层级取。