[{"data":1,"prerenderedAt":1940},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002Ffind-gaps-and-overlaps-between-polygons-pyqgis":3},{"id":4,"title":5,"body":6,"description":1929,"extension":1930,"meta":1931,"navigation":223,"path":1936,"seo":1937,"stem":1938,"__hash__":1939},"docs\u002Fspatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002Ffind-gaps-and-overlaps-between-polygons-pyqgis\u002Findex.md","Find Gaps and Overlaps Between Polygons in PyQGIS",{"type":7,"value":8,"toc":1915},"minimark",[9,13,17,26,149,154,177,181,184,645,664,668,671,761,1025,1030,1034,1037,1153,1158,1162,1165,1213,1380,1385,1389,1392,1558,1572,1576,1579,1756,1770,1774,1803,1807,1836,1840,1843,1847,1853,1859,1868,1878,1882,1911],[10,11,5],"h1",{"id":12},"find-gaps-and-overlaps-between-polygons-in-pyqgis",[14,15,16],"p",{},"Many polygon layers are meant to be coverages: parcels, land-use zones, school catchments, census areas. In a coverage every point of the area belongs to exactly one polygon — no two polygons overlap and there are no holes between them. Real data breaks that rule constantly. Two neighbouring parcels digitized separately overlap by a few centimetres along their shared edge; a zoning update leaves a thin sliver of nothing between two zones; an imported layer double-counts a building plot. Every one of those defects distorts area totals, spatial joins and overlays.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002F","Data Quality & Topology Validation",". It finds overlaps between pairs of polygons, finds gaps inside the covered area, filters out numerical noise with sensible tolerances, and writes both to layers you can review.",[14,27,28],{},[29,30,35,39,43,50,59,67,72,77,82,87,94,101,107,113,116,120,124,128,132,136,144],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 280","img","Neighbouring polygons showing an overlap counted twice, a gap belonging to no polygon, and a tiny sliver that a tolerance filters out","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Overlap, gap and sliver",[40,41,42],"desc",{},"Four neighbouring polygons. Two of them overlap along their shared edge, shown as a highlighted lens shape counted twice in any area total. Between two others is a thin gap, an unassigned strip inside the covered area. A tiny sliver of overlap only millimetres wide is numerical noise from separate digitizing and is filtered by an area tolerance.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","280","#f6f3ea",[51,52,58],"text",{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"380","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","What a broken coverage looks like",[60,61],"polygon",{"points":62,"fill":63,"fillOpacity":64,"stroke":65,"style":66},"40,60 230,60 238,170 40,170","#eef7f4",0.85,"#0f766e","stroke-width:2",[60,68],{"points":69,"fill":70,"fillOpacity":64,"stroke":71,"style":66},"222,60 420,60 420,170 230,170","#eff3ff","#2563eb",[60,73],{"points":74,"fill":75,"fillOpacity":64,"stroke":76,"style":66},"40,180 228,180 228,260 40,260","#e8efe6","#15803d",[60,78],{"points":79,"fill":80,"fillOpacity":64,"stroke":81,"style":66},"240,180 420,180 420,260 240,260","#fdf2e2","#b45309",[60,83],{"points":84,"fill":85,"fillOpacity":64,"stroke":85,"style":86},"222,60 230,60 238,170 230,170","#b91c1c","stroke-width:1",[44,88],{"x":89,"y":90,"width":91,"height":92,"rx":46,"fill":49,"stroke":81,"style":93},"228","180","12","80","stroke-width:1.5",[44,95],{"x":96,"y":97,"width":98,"height":99,"rx":100,"fill":80,"stroke":85,"style":66},"470","52","266","60","8",[51,102,106],{"x":103,"y":104,"style":105,"fill":85,"textAnchor":57},"603","75.805","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","overlap",[51,108,112],{"x":103,"y":109,"style":110,"fill":111,"textAnchor":57},"95.755","text-anchor:middle;font-size:10.5px;font-family:sans-serif","#59645f","counted twice in area totals",[44,114],{"x":96,"y":115,"width":98,"height":99,"rx":100,"fill":80,"stroke":81,"style":66},"126",[51,117,119],{"x":103,"y":118,"style":105,"fill":81,"textAnchor":57},"149.805","gap",[51,121,123],{"x":103,"y":122,"style":110,"fill":111,"textAnchor":57},"169.755","belongs to no polygon",[44,125],{"x":96,"y":126,"width":98,"height":99,"rx":100,"fill":127,"stroke":111,"style":66},"200","#fffdf7",[51,129,131],{"x":103,"y":130,"style":105,"fill":56,"textAnchor":57},"223.805","sliver \u003C tolerance",[51,133,135],{"x":103,"y":134,"style":110,"fill":111,"textAnchor":57},"243.755","noise: filter by area",[137,138],"line",{"x1":139,"y1":140,"x2":141,"y2":142,"stroke":85,"style":143},"238","110","466","82","stroke-width:1.2;stroke-dasharray:4 3",[137,145],{"x1":146,"y1":147,"x2":141,"y2":148,"stroke":81,"style":143},"240","220","156",[150,151,153],"h2",{"id":152},"prerequisites","Prerequisites",[155,156,157,161,169],"ul",{},[158,159,160],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series.",[158,162,163,164,168],{},"A polygon layer in a projected CRS so that areas and tolerances are in metres. If the layer is geographic, reproject first — see ",[21,165,167],{"href":166},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fchoose-projected-crs-for-analysis-pyqgis\u002F","choosing a projected CRS for analysis",".",[158,170,171,172,176],{},"Valid geometries. Run a ",[21,173,175],{"href":174},"\u002Fspatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002Fcheck-geometry-validity-report-pyqgis\u002F","validity report"," first; overlay operations on invalid polygons give unreliable results.",[150,178,180],{"id":179},"find-overlaps-with-a-spatial-index","Find overlaps with a spatial index",[14,182,183],{},"An overlap is the intersection of two polygons that has area. Testing every pair would be quadratic; a spatial index reduces the candidates to neighbours whose bounding boxes touch.",[185,186,191],"pre",{"className":187,"code":188,"language":189,"meta":190,"style":190},"language-python shiki shiki-themes github-dark","from qgis.core import (QgsProject, QgsSpatialIndex, QgsFeatureRequest,\n                       QgsVectorLayer, QgsFeature, QgsWkbTypes)\n\nparcels = QgsProject.instance().mapLayersByName(\"parcels\")[0]\nMIN_AREA = 0.5     # m² — smaller overlaps are digitizing noise\n\ngeoms = {f.id(): f.geometry() for f in parcels.getFeatures()}\nkeys = {f.id(): f[\"parcel_id\"] for f in parcels.getFeatures(\n    QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry))}\nindex = QgsSpatialIndex(parcels.getFeatures(),\n                        flags=QgsSpatialIndex.FlagStoreFeatureGeometries)\n\noverlaps = QgsVectorLayer(\n    f\"MultiPolygon?crs={parcels.crs().authid()}\"\n    \"&field=a:string&field=b:string&field=area_m2:double\", \"overlaps\", \"memory\")\nout = []\nfor fid, geom in geoms.items():\n    engine = QgsGeometry.createGeometryEngine(geom.constGet())\n    engine.prepareGeometry()\n    for other in index.intersects(geom.boundingBox()):\n        if other \u003C= fid:\n            continue                       # each pair once\n        g2 = geoms[other]\n        if not engine.intersects(g2.constGet()):\n            continue\n        inter = geom.intersection(g2)\n        if inter.type() != QgsWkbTypes.PolygonGeometry or inter.area() \u003C MIN_AREA:\n            continue\n        f = QgsFeature(overlaps.fields())\n        f.setAttributes([keys[fid], keys[other], round(inter.area(), 2)])\n        inter.convertToMultiType()\n        f.setGeometry(inter)\n        out.append(f)\noverlaps.dataProvider().addFeatures(out)\nQgsProject.instance().addMapLayer(overlaps)\nprint(len(out), \"overlaps above\", MIN_AREA, \"m²\")\n","python","",[192,193,194,212,218,225,250,266,271,294,320,326,337,349,354,365,386,406,417,430,441,447,461,475,484,495,506,512,523,552,557,568,586,592,598,604,610,616],"code",{"__ignoreMap":190},[195,196,198,202,206,209],"span",{"class":137,"line":197},1,[195,199,201],{"class":200},"snl16","from",[195,203,205],{"class":204},"s95oV"," qgis.core ",[195,207,208],{"class":200},"import",[195,210,211],{"class":204}," (QgsProject, QgsSpatialIndex, QgsFeatureRequest,\n",[195,213,215],{"class":137,"line":214},2,[195,216,217],{"class":204},"                       QgsVectorLayer, QgsFeature, QgsWkbTypes)\n",[195,219,221],{"class":137,"line":220},3,[195,222,224],{"emptyLinePlaceholder":223},true,"\n",[195,226,228,231,234,237,241,244,247],{"class":137,"line":227},4,[195,229,230],{"class":204},"parcels ",[195,232,233],{"class":200},"=",[195,235,236],{"class":204}," QgsProject.instance().mapLayersByName(",[195,238,240],{"class":239},"sU2Wk","\"parcels\"",[195,242,243],{"class":204},")[",[195,245,46],{"class":246},"sDLfK",[195,248,249],{"class":204},"]\n",[195,251,253,256,259,262],{"class":137,"line":252},5,[195,254,255],{"class":246},"MIN_AREA",[195,257,258],{"class":200}," =",[195,260,261],{"class":246}," 0.5",[195,263,265],{"class":264},"sjoCn","     # m² — smaller overlaps are digitizing noise\n",[195,267,269],{"class":137,"line":268},6,[195,270,224],{"emptyLinePlaceholder":223},[195,272,274,277,279,282,285,288,291],{"class":137,"line":273},7,[195,275,276],{"class":204},"geoms ",[195,278,233],{"class":200},[195,280,281],{"class":204}," {f.id(): f.geometry() ",[195,283,284],{"class":200},"for",[195,286,287],{"class":204}," f ",[195,289,290],{"class":200},"in",[195,292,293],{"class":204}," parcels.getFeatures()}\n",[195,295,297,300,302,305,308,311,313,315,317],{"class":137,"line":296},8,[195,298,299],{"class":204},"keys ",[195,301,233],{"class":200},[195,303,304],{"class":204}," {f.id(): f[",[195,306,307],{"class":239},"\"parcel_id\"",[195,309,310],{"class":204},"] ",[195,312,284],{"class":200},[195,314,287],{"class":204},[195,316,290],{"class":200},[195,318,319],{"class":204}," parcels.getFeatures(\n",[195,321,323],{"class":137,"line":322},9,[195,324,325],{"class":204},"    QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry))}\n",[195,327,329,332,334],{"class":137,"line":328},10,[195,330,331],{"class":204},"index ",[195,333,233],{"class":200},[195,335,336],{"class":204}," QgsSpatialIndex(parcels.getFeatures(),\n",[195,338,340,344,346],{"class":137,"line":339},11,[195,341,343],{"class":342},"s9osk","                        flags",[195,345,233],{"class":200},[195,347,348],{"class":204},"QgsSpatialIndex.FlagStoreFeatureGeometries)\n",[195,350,352],{"class":137,"line":351},12,[195,353,224],{"emptyLinePlaceholder":223},[195,355,357,360,362],{"class":137,"line":356},13,[195,358,359],{"class":204},"overlaps ",[195,361,233],{"class":200},[195,363,364],{"class":204}," QgsVectorLayer(\n",[195,366,368,371,374,377,380,383],{"class":137,"line":367},14,[195,369,370],{"class":200},"    f",[195,372,373],{"class":239},"\"MultiPolygon?crs=",[195,375,376],{"class":246},"{",[195,378,379],{"class":204},"parcels.crs().authid()",[195,381,382],{"class":246},"}",[195,384,385],{"class":239},"\"\n",[195,387,389,392,395,398,400,403],{"class":137,"line":388},15,[195,390,391],{"class":239},"    \"&field=a:string&field=b:string&field=area_m2:double\"",[195,393,394],{"class":204},", ",[195,396,397],{"class":239},"\"overlaps\"",[195,399,394],{"class":204},[195,401,402],{"class":239},"\"memory\"",[195,404,405],{"class":204},")\n",[195,407,409,412,414],{"class":137,"line":408},16,[195,410,411],{"class":204},"out ",[195,413,233],{"class":200},[195,415,416],{"class":204}," []\n",[195,418,420,422,425,427],{"class":137,"line":419},17,[195,421,284],{"class":200},[195,423,424],{"class":204}," fid, geom ",[195,426,290],{"class":200},[195,428,429],{"class":204}," geoms.items():\n",[195,431,433,436,438],{"class":137,"line":432},18,[195,434,435],{"class":204},"    engine ",[195,437,233],{"class":200},[195,439,440],{"class":204}," QgsGeometry.createGeometryEngine(geom.constGet())\n",[195,442,444],{"class":137,"line":443},19,[195,445,446],{"class":204},"    engine.prepareGeometry()\n",[195,448,450,453,456,458],{"class":137,"line":449},20,[195,451,452],{"class":200},"    for",[195,454,455],{"class":204}," other ",[195,457,290],{"class":200},[195,459,460],{"class":204}," index.intersects(geom.boundingBox()):\n",[195,462,464,467,469,472],{"class":137,"line":463},21,[195,465,466],{"class":200},"        if",[195,468,455],{"class":204},[195,470,471],{"class":200},"\u003C=",[195,473,474],{"class":204}," fid:\n",[195,476,478,481],{"class":137,"line":477},22,[195,479,480],{"class":200},"            continue",[195,482,483],{"class":264},"                       # each pair once\n",[195,485,487,490,492],{"class":137,"line":486},23,[195,488,489],{"class":204},"        g2 ",[195,491,233],{"class":200},[195,493,494],{"class":204}," geoms[other]\n",[195,496,498,500,503],{"class":137,"line":497},24,[195,499,466],{"class":200},[195,501,502],{"class":200}," not",[195,504,505],{"class":204}," engine.intersects(g2.constGet()):\n",[195,507,509],{"class":137,"line":508},25,[195,510,511],{"class":200},"            continue\n",[195,513,515,518,520],{"class":137,"line":514},26,[195,516,517],{"class":204},"        inter ",[195,519,233],{"class":200},[195,521,522],{"class":204}," geom.intersection(g2)\n",[195,524,526,528,531,534,537,540,543,546,549],{"class":137,"line":525},27,[195,527,466],{"class":200},[195,529,530],{"class":204}," inter.type() ",[195,532,533],{"class":200},"!=",[195,535,536],{"class":204}," QgsWkbTypes.PolygonGeometry ",[195,538,539],{"class":200},"or",[195,541,542],{"class":204}," inter.area() ",[195,544,545],{"class":200},"\u003C",[195,547,548],{"class":246}," MIN_AREA",[195,550,551],{"class":204},":\n",[195,553,555],{"class":137,"line":554},28,[195,556,511],{"class":200},[195,558,560,563,565],{"class":137,"line":559},29,[195,561,562],{"class":204},"        f ",[195,564,233],{"class":200},[195,566,567],{"class":204}," QgsFeature(overlaps.fields())\n",[195,569,571,574,577,580,583],{"class":137,"line":570},30,[195,572,573],{"class":204},"        f.setAttributes([keys[fid], keys[other], ",[195,575,576],{"class":246},"round",[195,578,579],{"class":204},"(inter.area(), ",[195,581,582],{"class":246},"2",[195,584,585],{"class":204},")])\n",[195,587,589],{"class":137,"line":588},31,[195,590,591],{"class":204},"        inter.convertToMultiType()\n",[195,593,595],{"class":137,"line":594},32,[195,596,597],{"class":204},"        f.setGeometry(inter)\n",[195,599,601],{"class":137,"line":600},33,[195,602,603],{"class":204},"        out.append(f)\n",[195,605,607],{"class":137,"line":606},34,[195,608,609],{"class":204},"overlaps.dataProvider().addFeatures(out)\n",[195,611,613],{"class":137,"line":612},35,[195,614,615],{"class":204},"QgsProject.instance().addMapLayer(overlaps)\n",[195,617,619,622,625,628,631,634,636,638,640,643],{"class":137,"line":618},36,[195,620,621],{"class":246},"print",[195,623,624],{"class":204},"(",[195,626,627],{"class":246},"len",[195,629,630],{"class":204},"(out), ",[195,632,633],{"class":239},"\"overlaps above\"",[195,635,394],{"class":204},[195,637,255],{"class":246},[195,639,394],{"class":204},[195,641,642],{"class":239},"\"m²\"",[195,644,405],{"class":204},[14,646,647,651,652,655,656,659,660,663],{},[648,649,650],"strong",{},"Breakdown:"," ",[192,653,654],{},"other \u003C= fid"," ensures each pair is tested once, halving the work and avoiding duplicate output. A prepared geometry engine makes repeated ",[192,657,658],{},"intersects"," tests against the same polygon much faster, which matters because each polygon is tested against all its neighbours. Neighbours that only share an edge intersect, but the intersection is a line, not an area; checking the result's type filters those out. The area threshold separates real overlaps from floating-point noise along shared edges. Add ",[192,661,662],{},"from qgis.core import QgsGeometry"," if running the snippet on its own.",[150,665,667],{"id":666},"find-gaps-by-dissolving-and-differencing","Find gaps by dissolving and differencing",[14,669,670],{},"A gap is space inside the covered area that no polygon claims. The trick is to define \"inside\": dissolve everything into one shape, then holes in the dissolved shape are gaps — but so is the space between islands of coverage, and the outer boundary is not.",[14,672,673],{},[29,674,677,680,683,686,702,705,709,714,718,722,727,730,733,736,739,743,746,751,754,757],{"viewBox":675,"role":32,"ariaLabel":676,"xmlns":34},"0 0 760 250","Gaps found by dissolving the coverage into one shape, taking its interior holes, and dropping holes smaller than a tolerance",[36,678,679],{},"Gaps are holes in the dissolved coverage",[40,681,682],{},"Step one: dissolve all polygons into one multipolygon. Step two: take its interior rings, the holes, each of which is an area surrounded by coverage but claimed by no polygon. Step three: drop holes smaller than the tolerance and keep the rest as gap polygons. The outside of the coverage is never a gap.",[44,684],{"x":46,"y":46,"width":47,"height":685,"fill":49},"250",[687,688,689],"defs",{},[690,691,697],"marker",{"id":692,"viewBox":693,"refX":100,"refY":694,"markerWidth":695,"markerHeight":695,"orient":696},"gapMethodArrow","0 0 10 10","5","7","auto-start-reverse",[698,699],"path",{"d":700,"fill":701},"M0 0 L10 5 L0 10 z","#2f3b35",[51,703,704],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Dissolve, take holes, filter",[44,706],{"x":707,"y":99,"width":147,"height":708,"rx":100,"fill":63,"stroke":65,"style":66},"24","150",[51,710,713],{"x":711,"y":712,"style":105,"fill":65,"textAnchor":57},"134","112.78","1. dissolve",[51,715,717],{"x":711,"y":716,"style":110,"fill":701,"textAnchor":57},"138.78","native:dissolve",[51,719,721],{"x":711,"y":720,"style":110,"fill":111,"textAnchor":57},"164.78","all polygons → one",[137,723],{"x1":724,"y1":725,"x2":98,"y2":725,"stroke":701,"style":726},"244","135","stroke-width:1.8;marker-end:url(#gapMethodArrow)",[44,728],{"x":729,"y":99,"width":147,"height":708,"rx":100,"fill":80,"stroke":81,"style":66},"270",[51,731,732],{"x":53,"y":712,"style":105,"fill":81,"textAnchor":57},"2. interior rings",[51,734,735],{"x":53,"y":716,"style":110,"fill":701,"textAnchor":57},"holes of the result",[51,737,738],{"x":53,"y":720,"style":110,"fill":111,"textAnchor":57},"surrounded, unclaimed",[137,740],{"x1":741,"y1":725,"x2":742,"y2":725,"stroke":701,"style":726},"490","512",[44,744],{"x":745,"y":99,"width":147,"height":708,"rx":100,"fill":75,"stroke":76,"style":66},"516",[51,747,750],{"x":748,"y":712,"style":105,"fill":749,"textAnchor":57},"626","#166534","3. filter",[51,752,753],{"x":748,"y":716,"style":110,"fill":701,"textAnchor":57},"area ≥ tolerance",[51,755,756],{"x":748,"y":720,"style":110,"fill":111,"textAnchor":57},"keep as gap polygons",[51,758,760],{"x":53,"y":759,"style":110,"fill":111,"textAnchor":57},"236","the outside of the coverage is never a gap",[185,762,764],{"className":187,"code":763,"language":189,"meta":190,"style":190},"import processing\nfrom qgis.core import QgsGeometry, QgsPolygon, QgsLineString\n\ndissolved = processing.run(\"native:dissolve\", {\n    \"INPUT\": parcels, \"FIELD\": [], \"OUTPUT\": \"memory:\"})[\"OUTPUT\"]\ncover = next(dissolved.getFeatures()).geometry()\n\ngaps = QgsVectorLayer(\n    f\"Polygon?crs={parcels.crs().authid()}&field=area_m2:double\", \"gaps\", \"memory\")\nfound = []\nfor part in cover.constParts():\n    for i in range(part.numInteriorRings()):\n        ring = part.interiorRing(i).clone()\n        hole = QgsGeometry(QgsPolygon(ring))\n        if hole.area() >= MIN_AREA:\n            f = QgsFeature(gaps.fields())\n            f.setAttributes([round(hole.area(), 2)])\n            f.setGeometry(hole)\n            found.append(f)\ngaps.dataProvider().addFeatures(found)\nQgsProject.instance().addMapLayer(gaps)\nprint(len(found), \"gaps above\", MIN_AREA, \"m²\")\n",[192,765,766,773,784,788,804,834,847,851,860,887,896,908,923,933,943,957,967,981,986,991,996,1001],{"__ignoreMap":190},[195,767,768,770],{"class":137,"line":197},[195,769,208],{"class":200},[195,771,772],{"class":204}," processing\n",[195,774,775,777,779,781],{"class":137,"line":214},[195,776,201],{"class":200},[195,778,205],{"class":204},[195,780,208],{"class":200},[195,782,783],{"class":204}," QgsGeometry, QgsPolygon, QgsLineString\n",[195,785,786],{"class":137,"line":220},[195,787,224],{"emptyLinePlaceholder":223},[195,789,790,793,795,798,801],{"class":137,"line":227},[195,791,792],{"class":204},"dissolved ",[195,794,233],{"class":200},[195,796,797],{"class":204}," processing.run(",[195,799,800],{"class":239},"\"native:dissolve\"",[195,802,803],{"class":204},", {\n",[195,805,806,809,812,815,818,821,824,827,830,832],{"class":137,"line":252},[195,807,808],{"class":239},"    \"INPUT\"",[195,810,811],{"class":204},": parcels, ",[195,813,814],{"class":239},"\"FIELD\"",[195,816,817],{"class":204},": [], ",[195,819,820],{"class":239},"\"OUTPUT\"",[195,822,823],{"class":204},": ",[195,825,826],{"class":239},"\"memory:\"",[195,828,829],{"class":204},"})[",[195,831,820],{"class":239},[195,833,249],{"class":204},[195,835,836,839,841,844],{"class":137,"line":268},[195,837,838],{"class":204},"cover ",[195,840,233],{"class":200},[195,842,843],{"class":246}," next",[195,845,846],{"class":204},"(dissolved.getFeatures()).geometry()\n",[195,848,849],{"class":137,"line":273},[195,850,224],{"emptyLinePlaceholder":223},[195,852,853,856,858],{"class":137,"line":296},[195,854,855],{"class":204},"gaps ",[195,857,233],{"class":200},[195,859,364],{"class":204},[195,861,862,864,867,869,871,873,876,878,881,883,885],{"class":137,"line":322},[195,863,370],{"class":200},[195,865,866],{"class":239},"\"Polygon?crs=",[195,868,376],{"class":246},[195,870,379],{"class":204},[195,872,382],{"class":246},[195,874,875],{"class":239},"&field=area_m2:double\"",[195,877,394],{"class":204},[195,879,880],{"class":239},"\"gaps\"",[195,882,394],{"class":204},[195,884,402],{"class":239},[195,886,405],{"class":204},[195,888,889,892,894],{"class":137,"line":328},[195,890,891],{"class":204},"found ",[195,893,233],{"class":200},[195,895,416],{"class":204},[195,897,898,900,903,905],{"class":137,"line":339},[195,899,284],{"class":200},[195,901,902],{"class":204}," part ",[195,904,290],{"class":200},[195,906,907],{"class":204}," cover.constParts():\n",[195,909,910,912,915,917,920],{"class":137,"line":351},[195,911,452],{"class":200},[195,913,914],{"class":204}," i ",[195,916,290],{"class":200},[195,918,919],{"class":246}," range",[195,921,922],{"class":204},"(part.numInteriorRings()):\n",[195,924,925,928,930],{"class":137,"line":356},[195,926,927],{"class":204},"        ring ",[195,929,233],{"class":200},[195,931,932],{"class":204}," part.interiorRing(i).clone()\n",[195,934,935,938,940],{"class":137,"line":367},[195,936,937],{"class":204},"        hole ",[195,939,233],{"class":200},[195,941,942],{"class":204}," QgsGeometry(QgsPolygon(ring))\n",[195,944,945,947,950,953,955],{"class":137,"line":388},[195,946,466],{"class":200},[195,948,949],{"class":204}," hole.area() ",[195,951,952],{"class":200},">=",[195,954,548],{"class":246},[195,956,551],{"class":204},[195,958,959,962,964],{"class":137,"line":408},[195,960,961],{"class":204},"            f ",[195,963,233],{"class":200},[195,965,966],{"class":204}," QgsFeature(gaps.fields())\n",[195,968,969,972,974,977,979],{"class":137,"line":419},[195,970,971],{"class":204},"            f.setAttributes([",[195,973,576],{"class":246},[195,975,976],{"class":204},"(hole.area(), ",[195,978,582],{"class":246},[195,980,585],{"class":204},[195,982,983],{"class":137,"line":432},[195,984,985],{"class":204},"            f.setGeometry(hole)\n",[195,987,988],{"class":137,"line":443},[195,989,990],{"class":204},"            found.append(f)\n",[195,992,993],{"class":137,"line":449},[195,994,995],{"class":204},"gaps.dataProvider().addFeatures(found)\n",[195,997,998],{"class":137,"line":463},[195,999,1000],{"class":204},"QgsProject.instance().addMapLayer(gaps)\n",[195,1002,1003,1005,1007,1009,1012,1015,1017,1019,1021,1023],{"class":137,"line":477},[195,1004,621],{"class":246},[195,1006,624],{"class":204},[195,1008,627],{"class":246},[195,1010,1011],{"class":204},"(found), ",[195,1013,1014],{"class":239},"\"gaps above\"",[195,1016,394],{"class":204},[195,1018,255],{"class":246},[195,1020,394],{"class":204},[195,1022,642],{"class":239},[195,1024,405],{"class":204},[14,1026,1027,1029],{},[648,1028,650],{}," Dissolving without a field merges every polygon into one multipolygon. Each part's interior rings are holes the coverage surrounds but no polygon fills — exactly the definition of a gap. Converting a ring into a polygon makes its area measurable and gives you something to draw. Holes that are intentional — a lake inside a municipal boundary layer, a courtyard inside a building footprint — appear here too; if the dataset has legitimate holes, compare against a layer of known holes or check the attributes of surrounding polygons before treating every result as an error.",[150,1031,1033],{"id":1032},"gaps-between-separate-islands","Gaps between separate islands",[14,1035,1036],{},"Interior rings only catch gaps that are fully enclosed. A strip of missing coverage that reaches the edge of the area — between two parishes along a county boundary, say — is not a hole in the dissolve. For those, compare against the boundary the coverage should fill.",[185,1038,1040],{"className":187,"code":1039,"language":189,"meta":190,"style":190},"county = QgsProject.instance().mapLayersByName(\"county_boundary\")[0]\nexpected = next(county.getFeatures()).geometry()\n\nedge_gaps = expected.difference(cover)\npieces = [QgsGeometry(p.clone()) for p in edge_gaps.constParts()]\nreal = [p for p in pieces if p.area() >= MIN_AREA]\nprint(len(real), \"uncovered areas inside the county boundary\")\n",[192,1041,1042,1060,1072,1076,1086,1106,1137],{"__ignoreMap":190},[195,1043,1044,1047,1049,1051,1054,1056,1058],{"class":137,"line":197},[195,1045,1046],{"class":204},"county ",[195,1048,233],{"class":200},[195,1050,236],{"class":204},[195,1052,1053],{"class":239},"\"county_boundary\"",[195,1055,243],{"class":204},[195,1057,46],{"class":246},[195,1059,249],{"class":204},[195,1061,1062,1065,1067,1069],{"class":137,"line":214},[195,1063,1064],{"class":204},"expected ",[195,1066,233],{"class":200},[195,1068,843],{"class":246},[195,1070,1071],{"class":204},"(county.getFeatures()).geometry()\n",[195,1073,1074],{"class":137,"line":220},[195,1075,224],{"emptyLinePlaceholder":223},[195,1077,1078,1081,1083],{"class":137,"line":227},[195,1079,1080],{"class":204},"edge_gaps ",[195,1082,233],{"class":200},[195,1084,1085],{"class":204}," expected.difference(cover)\n",[195,1087,1088,1091,1093,1096,1098,1101,1103],{"class":137,"line":252},[195,1089,1090],{"class":204},"pieces ",[195,1092,233],{"class":200},[195,1094,1095],{"class":204}," [QgsGeometry(p.clone()) ",[195,1097,284],{"class":200},[195,1099,1100],{"class":204}," p ",[195,1102,290],{"class":200},[195,1104,1105],{"class":204}," edge_gaps.constParts()]\n",[195,1107,1108,1111,1113,1116,1118,1120,1122,1125,1128,1131,1133,1135],{"class":137,"line":268},[195,1109,1110],{"class":204},"real ",[195,1112,233],{"class":200},[195,1114,1115],{"class":204}," [p ",[195,1117,284],{"class":200},[195,1119,1100],{"class":204},[195,1121,290],{"class":200},[195,1123,1124],{"class":204}," pieces ",[195,1126,1127],{"class":200},"if",[195,1129,1130],{"class":204}," p.area() ",[195,1132,952],{"class":200},[195,1134,548],{"class":246},[195,1136,249],{"class":204},[195,1138,1139,1141,1143,1145,1148,1151],{"class":137,"line":273},[195,1140,621],{"class":246},[195,1142,624],{"class":204},[195,1144,627],{"class":246},[195,1146,1147],{"class":204},"(real), ",[195,1149,1150],{"class":239},"\"uncovered areas inside the county boundary\"",[195,1152,405],{"class":204},[14,1154,1155,1157],{},[648,1156,650],{}," Subtracting the dissolved coverage from the expected extent leaves everything the coverage fails to fill, enclosed or not. This needs an authoritative outline — the county boundary, the project area, the municipal extent — but when one exists it is the more complete test. Both layers must be in the same CRS; if they are not, transform one first. Thin pieces along the boundary itself usually mean the coverage and the outline were digitized at different scales, which is a separate conversation from internal gaps.",[150,1159,1161],{"id":1160},"tell-slivers-from-real-problems","Tell slivers from real problems",[14,1163,1164],{},"Area alone is a blunt filter: a long, thin overlap of 0.4 m² along a 200 m shared edge is noise, while a compact 0.4 m² overlap may be a duplicated corner point that matters. A thinness measure separates the two.",[14,1166,1167],{},[29,1168,1171,1174,1177,1179,1182,1187,1193,1199,1202,1208],{"viewBox":1169,"role":32,"ariaLabel":1170,"xmlns":34},"0 0 760 240","Two overlaps of equal area: a long thin sliver with a low thinness ratio and a compact blob with a high ratio",[36,1172,1173],{},"Thinness separates slivers from blobs",[40,1175,1176],{},"Two overlaps with the same area. A long thin sliver along a shared edge has a large perimeter for its area and a thinness ratio near zero; it is digitizing noise. A compact blob has a ratio near one and is likely a real error such as a duplicated or misplaced vertex. The ratio is four pi times area divided by perimeter squared.",[44,1178],{"x":46,"y":46,"width":47,"height":146,"fill":49},[51,1180,1181],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Same area, different meaning",[44,1183],{"x":1184,"y":1185,"width":1186,"height":100,"rx":582,"fill":85,"stroke":85,"style":93},"40","92","300",[51,1188,1192],{"x":1189,"y":1190,"style":1191,"fill":85,"textAnchor":57},"190","130","text-anchor:middle;font-size:11.0px;font-family:sans-serif","sliver: ratio ≈ 0.03 — noise",[1194,1195],"circle",{"cx":1196,"cy":1197,"r":1198,"fill":81,"stroke":81,"style":86},"560","96","26",[51,1200,1201],{"x":1196,"y":708,"style":1191,"fill":81,"textAnchor":57},"blob: ratio ≈ 0.9 — check",[44,1203],{"x":1204,"y":1205,"width":1206,"height":1207,"rx":100,"fill":127,"stroke":111,"style":66},"140","176","480","44",[51,1209,1212],{"x":53,"y":1210,"style":1211,"fill":701,"textAnchor":57},"201.78","text-anchor:middle;font-size:10.0px;font-family:monospace","thinness = 4π · area \u002F perimeter²",[185,1214,1216],{"className":187,"code":1215,"language":189,"meta":190,"style":190},"import math\n\ndef thinness(geom):\n    p = geom.length()\n    return 4 * math.pi * geom.area() \u002F (p * p) if p else 0\n\nfor f in overlaps.getFeatures():\n    t = thinness(f.geometry())\n    kind = \"sliver\" if t \u003C 0.1 else \"compact\"\n    print(f[\"a\"], f[\"b\"], f[\"area_m2\"], round(t, 3), kind)\n",[192,1217,1218,1225,1229,1241,1251,1292,1296,1307,1317,1344],{"__ignoreMap":190},[195,1219,1220,1222],{"class":137,"line":197},[195,1221,208],{"class":200},[195,1223,1224],{"class":204}," math\n",[195,1226,1227],{"class":137,"line":214},[195,1228,224],{"emptyLinePlaceholder":223},[195,1230,1231,1234,1238],{"class":137,"line":220},[195,1232,1233],{"class":200},"def",[195,1235,1237],{"class":1236},"svObZ"," thinness",[195,1239,1240],{"class":204},"(geom):\n",[195,1242,1243,1246,1248],{"class":137,"line":227},[195,1244,1245],{"class":204},"    p ",[195,1247,233],{"class":200},[195,1249,1250],{"class":204}," geom.length()\n",[195,1252,1253,1256,1259,1262,1265,1268,1271,1274,1277,1279,1282,1284,1286,1289],{"class":137,"line":252},[195,1254,1255],{"class":200},"    return",[195,1257,1258],{"class":246}," 4",[195,1260,1261],{"class":200}," *",[195,1263,1264],{"class":204}," math.pi ",[195,1266,1267],{"class":200},"*",[195,1269,1270],{"class":204}," geom.area() ",[195,1272,1273],{"class":200},"\u002F",[195,1275,1276],{"class":204}," (p ",[195,1278,1267],{"class":200},[195,1280,1281],{"class":204}," p) ",[195,1283,1127],{"class":200},[195,1285,1100],{"class":204},[195,1287,1288],{"class":200},"else",[195,1290,1291],{"class":246}," 0\n",[195,1293,1294],{"class":137,"line":268},[195,1295,224],{"emptyLinePlaceholder":223},[195,1297,1298,1300,1302,1304],{"class":137,"line":273},[195,1299,284],{"class":200},[195,1301,287],{"class":204},[195,1303,290],{"class":200},[195,1305,1306],{"class":204}," overlaps.getFeatures():\n",[195,1308,1309,1312,1314],{"class":137,"line":296},[195,1310,1311],{"class":204},"    t ",[195,1313,233],{"class":200},[195,1315,1316],{"class":204}," thinness(f.geometry())\n",[195,1318,1319,1322,1324,1327,1330,1333,1335,1338,1341],{"class":137,"line":322},[195,1320,1321],{"class":204},"    kind ",[195,1323,233],{"class":200},[195,1325,1326],{"class":239}," \"sliver\"",[195,1328,1329],{"class":200}," if",[195,1331,1332],{"class":204}," t ",[195,1334,545],{"class":200},[195,1336,1337],{"class":246}," 0.1",[195,1339,1340],{"class":200}," else",[195,1342,1343],{"class":239}," \"compact\"\n",[195,1345,1346,1349,1352,1355,1358,1361,1363,1366,1369,1371,1374,1377],{"class":137,"line":328},[195,1347,1348],{"class":246},"    print",[195,1350,1351],{"class":204},"(f[",[195,1353,1354],{"class":239},"\"a\"",[195,1356,1357],{"class":204},"], f[",[195,1359,1360],{"class":239},"\"b\"",[195,1362,1357],{"class":204},[195,1364,1365],{"class":239},"\"area_m2\"",[195,1367,1368],{"class":204},"], ",[195,1370,576],{"class":246},[195,1372,1373],{"class":204},"(t, ",[195,1375,1376],{"class":246},"3",[195,1378,1379],{"class":204},"), kind)\n",[14,1381,1382,1384],{},[648,1383,650],{}," The ratio — the isoperimetric quotient — is 1 for a circle and approaches 0 for long, thin shapes. Slivers along shared edges sit well below 0.1. Keeping both the area and the thinness on the output layer lets you style the results by kind and spend review time on the compact ones. A stricter workflow uses a negative buffer: shapes that vanish when buffered inward by a few centimetres are slivers by definition.",[150,1386,1388],{"id":1387},"summarise-coverage-health-in-one-line","Summarise coverage health in one line",[14,1390,1391],{},"Once the overlap and gap layers exist, a short summary makes the result comparable between runs and between datasets. The useful numbers are relative: what share of the covered area is double-counted, and what share is missing.",[185,1393,1395],{"className":187,"code":1394,"language":189,"meta":190,"style":190},"cover_area = cover.area()\noverlap_area = sum(f[\"area_m2\"] for f in overlaps.getFeatures())\ngap_area = sum(f[\"area_m2\"] for f in gaps.getFeatures())\n\nprint(f\"coverage {cover_area \u002F 1e6:.2f} km² | \"\n      f\"overlaps {len(out)} ({overlap_area \u002F cover_area:.4%}) | \"\n      f\"gaps {len(found)} ({gap_area \u002F cover_area:.4%})\")\n",[192,1396,1397,1407,1432,1456,1460,1489,1525],{"__ignoreMap":190},[195,1398,1399,1402,1404],{"class":137,"line":197},[195,1400,1401],{"class":204},"cover_area ",[195,1403,233],{"class":200},[195,1405,1406],{"class":204}," cover.area()\n",[195,1408,1409,1412,1414,1417,1419,1421,1423,1425,1427,1429],{"class":137,"line":214},[195,1410,1411],{"class":204},"overlap_area ",[195,1413,233],{"class":200},[195,1415,1416],{"class":246}," sum",[195,1418,1351],{"class":204},[195,1420,1365],{"class":239},[195,1422,310],{"class":204},[195,1424,284],{"class":200},[195,1426,287],{"class":204},[195,1428,290],{"class":200},[195,1430,1431],{"class":204}," overlaps.getFeatures())\n",[195,1433,1434,1437,1439,1441,1443,1445,1447,1449,1451,1453],{"class":137,"line":220},[195,1435,1436],{"class":204},"gap_area ",[195,1438,233],{"class":200},[195,1440,1416],{"class":246},[195,1442,1351],{"class":204},[195,1444,1365],{"class":239},[195,1446,310],{"class":204},[195,1448,284],{"class":200},[195,1450,287],{"class":204},[195,1452,290],{"class":200},[195,1454,1455],{"class":204}," gaps.getFeatures())\n",[195,1457,1458],{"class":137,"line":227},[195,1459,224],{"emptyLinePlaceholder":223},[195,1461,1462,1464,1466,1469,1472,1474,1476,1478,1481,1484,1486],{"class":137,"line":252},[195,1463,621],{"class":246},[195,1465,624],{"class":204},[195,1467,1468],{"class":200},"f",[195,1470,1471],{"class":239},"\"coverage ",[195,1473,376],{"class":246},[195,1475,1401],{"class":204},[195,1477,1273],{"class":200},[195,1479,1480],{"class":246}," 1e6",[195,1482,1483],{"class":200},":.2f",[195,1485,382],{"class":246},[195,1487,1488],{"class":239}," km² | \"\n",[195,1490,1491,1494,1497,1500,1503,1505,1508,1510,1512,1514,1517,1520,1522],{"class":137,"line":268},[195,1492,1493],{"class":200},"      f",[195,1495,1496],{"class":239},"\"overlaps ",[195,1498,1499],{"class":246},"{len",[195,1501,1502],{"class":204},"(out)",[195,1504,382],{"class":246},[195,1506,1507],{"class":239}," (",[195,1509,376],{"class":246},[195,1511,1411],{"class":204},[195,1513,1273],{"class":200},[195,1515,1516],{"class":204}," cover_area",[195,1518,1519],{"class":200},":.4%",[195,1521,382],{"class":246},[195,1523,1524],{"class":239},") | \"\n",[195,1526,1527,1529,1532,1534,1537,1539,1541,1543,1545,1547,1549,1551,1553,1556],{"class":137,"line":273},[195,1528,1493],{"class":200},[195,1530,1531],{"class":239},"\"gaps ",[195,1533,1499],{"class":246},[195,1535,1536],{"class":204},"(found)",[195,1538,382],{"class":246},[195,1540,1507],{"class":239},[195,1542,376],{"class":246},[195,1544,1436],{"class":204},[195,1546,1273],{"class":200},[195,1548,1516],{"class":204},[195,1550,1519],{"class":200},[195,1552,382],{"class":246},[195,1554,1555],{"class":239},")\"",[195,1557,405],{"class":204},[14,1559,1560,1562,1563,1567,1568,1571],{},[648,1561,650],{}," Expressing overlap and gap areas as fractions of the coverage turns raw counts into a quality measure that scales: three hundred overlaps sound alarming until you see they total 0.002 % of the area, while twelve gaps covering 0.4 % of a cadastre is a serious finding. Recording these figures with a date each time the check runs — in a log table, or in the ",[21,1564,1566],{"href":1565},"\u002Fspatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002Fbuild-data-quality-report-pyqgis\u002F","data quality report"," — shows whether editing practice is improving. If the coverage should equal a known total, such as the official area of a municipality, compare ",[192,1569,1570],{},"cover_area"," with it as well; a large difference points to missing polygons rather than small gaps.",[150,1573,1575],{"id":1574},"fix-what-you-found","Fix what you found",[14,1577,1578],{},"The right fix depends on which polygon is authoritative. For slivers, snapping both layers to a common tolerance often removes them; for real overlaps, one polygon must yield to the other.",[185,1580,1582],{"className":187,"code":1581,"language":189,"meta":190,"style":190},"# remove overlaps by giving priority to the polygon with the earlier survey date\nwith edit(parcels):\n    for f in overlaps.getFeatures():\n        a = next(parcels.getFeatures(f\"\\\"parcel_id\\\" = '{f['a']}'\"))\n        b = next(parcels.getFeatures(f\"\\\"parcel_id\\\" = '{f['b']}'\"))\n        loser = b if a[\"surveyed\"] \u003C= b[\"surveyed\"] else a\n        winner = a if loser is b else b\n        parcels.changeGeometry(loser.id(), loser.geometry().difference(winner.geometry()))\n",[192,1583,1584,1589,1597,1607,1654,1692,1726,1751],{"__ignoreMap":190},[195,1585,1586],{"class":137,"line":197},[195,1587,1588],{"class":264},"# remove overlaps by giving priority to the polygon with the earlier survey date\n",[195,1590,1591,1594],{"class":137,"line":214},[195,1592,1593],{"class":200},"with",[195,1595,1596],{"class":204}," edit(parcels):\n",[195,1598,1599,1601,1603,1605],{"class":137,"line":220},[195,1600,452],{"class":200},[195,1602,287],{"class":204},[195,1604,290],{"class":200},[195,1606,1306],{"class":204},[195,1608,1609,1612,1614,1616,1619,1621,1624,1627,1630,1632,1635,1637,1640,1643,1646,1648,1651],{"class":137,"line":227},[195,1610,1611],{"class":204},"        a ",[195,1613,233],{"class":200},[195,1615,843],{"class":246},[195,1617,1618],{"class":204},"(parcels.getFeatures(",[195,1620,1468],{"class":200},[195,1622,1623],{"class":239},"\"",[195,1625,1626],{"class":246},"\\\"",[195,1628,1629],{"class":239},"parcel_id",[195,1631,1626],{"class":246},[195,1633,1634],{"class":239}," = '",[195,1636,376],{"class":246},[195,1638,1639],{"class":204},"f[",[195,1641,1642],{"class":239},"'a'",[195,1644,1645],{"class":204},"]",[195,1647,382],{"class":246},[195,1649,1650],{"class":239},"'\"",[195,1652,1653],{"class":204},"))\n",[195,1655,1656,1659,1661,1663,1665,1667,1669,1671,1673,1675,1677,1679,1681,1684,1686,1688,1690],{"class":137,"line":252},[195,1657,1658],{"class":204},"        b ",[195,1660,233],{"class":200},[195,1662,843],{"class":246},[195,1664,1618],{"class":204},[195,1666,1468],{"class":200},[195,1668,1623],{"class":239},[195,1670,1626],{"class":246},[195,1672,1629],{"class":239},[195,1674,1626],{"class":246},[195,1676,1634],{"class":239},[195,1678,376],{"class":246},[195,1680,1639],{"class":204},[195,1682,1683],{"class":239},"'b'",[195,1685,1645],{"class":204},[195,1687,382],{"class":246},[195,1689,1650],{"class":239},[195,1691,1653],{"class":204},[195,1693,1694,1697,1699,1702,1704,1707,1710,1712,1714,1717,1719,1721,1723],{"class":137,"line":268},[195,1695,1696],{"class":204},"        loser ",[195,1698,233],{"class":200},[195,1700,1701],{"class":204}," b ",[195,1703,1127],{"class":200},[195,1705,1706],{"class":204}," a[",[195,1708,1709],{"class":239},"\"surveyed\"",[195,1711,310],{"class":204},[195,1713,471],{"class":200},[195,1715,1716],{"class":204}," b[",[195,1718,1709],{"class":239},[195,1720,310],{"class":204},[195,1722,1288],{"class":200},[195,1724,1725],{"class":204}," a\n",[195,1727,1728,1731,1733,1736,1738,1741,1744,1746,1748],{"class":137,"line":273},[195,1729,1730],{"class":204},"        winner ",[195,1732,233],{"class":200},[195,1734,1735],{"class":204}," a ",[195,1737,1127],{"class":200},[195,1739,1740],{"class":204}," loser ",[195,1742,1743],{"class":200},"is",[195,1745,1701],{"class":204},[195,1747,1288],{"class":200},[195,1749,1750],{"class":204}," b\n",[195,1752,1753],{"class":137,"line":296},[195,1754,1755],{"class":204},"        parcels.changeGeometry(loser.id(), loser.geometry().difference(winner.geometry()))\n",[14,1757,1758,1760,1761,1765,1766,1769],{},[648,1759,650],{}," Choosing the winner by a rule — earlier survey, higher accuracy class, official over provisional — keeps the fix explainable. Subtracting the winner from the loser removes the overlap without creating a gap, because the overlapping area stays with the winner. Run the check again afterwards: differencing can leave tiny slivers of its own, which ",[21,1762,1764],{"href":1763},"\u002Fspatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002Fsnap-geometries-to-layer-pyqgis\u002F","snapping geometries to a layer"," cleans up. Add ",[192,1767,1768],{},"from qgis.core import edit"," when running this on its own.",[150,1771,1773],{"id":1772},"qgis-version-compatibility","QGIS version compatibility",[14,1775,1776,1777,1780,1781,1784,1785,1787,1788,1791,1792,1795,1796,1799,1800,168],{},"The spatial index flag ",[192,1778,1779],{},"FlagStoreFeatureGeometries"," and ",[192,1782,1783],{},"createGeometryEngine"," exist from QGIS 3.0. ",[192,1786,717],{}," is stable across 3.x and 4. On QGIS 4, ",[192,1789,1790],{},"QgsWkbTypes.PolygonGeometry"," is ",[192,1793,1794],{},"Qgis.GeometryType.Polygon",", and ",[192,1797,1798],{},"QgsSpatialIndex.FlagStoreFeatureGeometries"," is reached through ",[192,1801,1802],{},"QgsSpatialIndex.Flag.FlagStoreFeatureGeometries",[150,1804,1806],{"id":1805},"troubleshooting","Troubleshooting",[155,1808,1809,1818,1824,1830],{},[158,1810,1811,1814,1815,1817],{},[648,1812,1813],{},"Thousands of tiny overlaps."," The layer was digitized without snapping; raise ",[192,1816,255],{}," or snap first.",[158,1819,1820,1823],{},[648,1821,1822],{},"No gaps found, but the map shows white strips."," The strips reach the outer edge; use the expected-boundary difference.",[158,1825,1826,1829],{},[648,1827,1828],{},"Dissolve is slow or fails."," Invalid geometries; fix them first, or dissolve in tiles for very large coverages.",[158,1831,1832,1835],{},[648,1833,1834],{},"Areas look wrong."," The layer is in a geographic CRS; reproject to a projected system.",[150,1837,1839],{"id":1838},"conclusion","Conclusion",[14,1841,1842],{},"Find overlaps pairwise with a spatial index and a prepared engine, find enclosed gaps as interior rings of the dissolved coverage and edge gaps by differencing against the expected boundary, separate slivers from real defects with area and thinness, and fix by a priority rule before re-checking.",[150,1844,1846],{"id":1845},"frequently-asked-questions","Frequently Asked Questions",[14,1848,1849,1852],{},[648,1850,1851],{},"Does QGIS have a built-in topology checker?","\nYes, the Topology Checker core plugin checks rules interactively. It is not exposed as a stable Python API, so scripts use the approach shown here.",[14,1854,1855,1858],{},[648,1856,1857],{},"What tolerance should I use?","\nTie it to the data's accuracy: if parcels are accurate to 10 cm, overlaps narrower than that along an edge are noise.",[14,1860,1861,1864,1865,1867],{},[648,1862,1863],{},"Can I check overlaps between two different layers?","\nYes. Build the index on one layer and iterate the other; drop the ",[192,1866,654],{}," test.",[14,1869,1870,1873,1874,1877],{},[648,1871,1872],{},"Is this faster in PostGIS?","\nFor millions of polygons, yes — ",[192,1875,1876],{},"ST_Intersection"," with a GiST index runs in the database.",[150,1879,1881],{"id":1880},"related","Related",[155,1883,1884,1889,1894,1899,1905],{},[158,1885,1886,1888],{},[21,1887,24],{"href":23}," — the guide this recipe belongs to",[158,1890,1891],{},[21,1892,1893],{"href":174},"Build a Geometry Validity Report in PyQGIS",[158,1895,1896],{},[21,1897,1898],{"href":1763},"Snap Geometries to a Layer in PyQGIS",[158,1900,1901],{},[21,1902,1904],{"href":1903},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fbuild-spatial-index-pyqgis\u002F","Build a Spatial Index in PyQGIS",[158,1906,1907],{},[21,1908,1910],{"href":1909},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fdissolve-features-by-attribute-pyqgis\u002F","Dissolve Features by Attribute in PyQGIS",[1912,1913,1914],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":190,"searchDepth":214,"depth":214,"links":1916},[1917,1918,1919,1920,1921,1922,1923,1924,1925,1926,1927,1928],{"id":152,"depth":214,"text":153},{"id":179,"depth":214,"text":180},{"id":666,"depth":214,"text":667},{"id":1032,"depth":214,"text":1033},{"id":1160,"depth":214,"text":1161},{"id":1387,"depth":214,"text":1388},{"id":1574,"depth":214,"text":1575},{"id":1772,"depth":214,"text":1773},{"id":1805,"depth":214,"text":1806},{"id":1838,"depth":214,"text":1839},{"id":1845,"depth":214,"text":1846},{"id":1880,"depth":214,"text":1881},"Detect slivers, overlaps and gaps in polygon coverages such as parcels, zoning or administrative areas — using a spatial index for overlaps, a dissolve-and-difference for gaps, area tolerances to ignore noise, and an output layer you can review and fix.","md",{"slug":1932,"type":1933,"breadcrumb":1934,"datePublished":1935,"dateModified":1935},"find-gaps-and-overlaps-between-polygons-pyqgis","article","Find Gaps and Overlaps","2026-10-02","\u002Fspatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002Ffind-gaps-and-overlaps-between-polygons-pyqgis",{"title":5,"description":1929},"spatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002Ffind-gaps-and-overlaps-between-polygons-pyqgis\u002Findex","8OWsU-BFh0I2vMFrrJznGT1F0TRzU_hlOB93_sqMSKY",1790966261468]