GIS数据GIS DATA

把村界拼成县界:两套数据套合校验实测

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

站内有两套互相独立的边界:一套是村界包,7,404 个村面管到临沂每个行政村;另一套是全球线,中国区第 4 级给了 2,891 个区县面,顶点稀疏、面向宏观底图。同一个沂水县,一边是 1,063 个村面溶解出的精细轮廓,一边是只有 198 个顶点的简化线——把前者拼成县界,跟后者叠在一起,到底差多少?本文逐县量给你:12 个县的中位缝隙 119 到 220 米,山区县全段吻合,个别城区藏着公里级的离群段。

先看周长:简化线偷偷切了多少角

套合之前,周长先暴露了差距。把每个县的村面按归属溶解成轮廓(有向边配对法,见《从村界聚合出乡镇边界:环缝合实测》),与全球线的县界周长两两相除:

县村界周长level4 周长比值中位缝隙≤500 米占比
沂水县613km341km1.80134.6m100%
兰陵县402km241km1.67125.1m91.9%
平邑县386km222km1.74124.9m100%
莒南县410km221km1.86131.7m96.0%
郯城县345km195km1.77173.8m87.1%
沂南县427km243km1.76129.5m97.2%
费县448km237km1.89119.0m95.9%
蒙阴县375km230km1.63133.8m100%
兰山区186km150km1.24142.4m93.9%
河东区174km167km1.04202.3m78.3%
临沭县308km173km1.78161.2m91.0%
罗庄区140km128km1.09220.0m60.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 公里(罗庄、河东、兰山、兰陵、费县、沂南都有)。这些不是随机噪声,是成段的偏离——城区的区划调整频繁(开发区代管、街道重划),两套数据对「这段线在哪」的口径出现了公里级分歧。中位数看全局,离群段看局部,两眼都要睁。

沂水县村界溶解轮廓与全球线简化县界套合对比图
沂水县全域与北部山区放大:橙色简化线大步切过青色村界轮廓的细碎弯折,中位缝隙约 135 米。

套合校验脚本:任何两套边界都这么量

整套校验三步,二十来行。第一步把细的一方溶解成轮廓(用《从村界聚合出乡镇边界》的无向配对净流量法,北京篇也用过);第二步给粗的一方建网格索引;第三步逐顶点求到最近线段的距离,出中位数和分位数:

// 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 秒跑完。村界包在「村界数据 · 按市」页取,全球线的区县界在「全球行政区划边界」页按国家与层级取。