[{"data":1,"prerenderedAt":1867},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002Fcheck-line-network-connectivity-pyqgis":3},{"id":4,"title":5,"body":6,"description":1856,"extension":1857,"meta":1858,"navigation":213,"path":1863,"seo":1864,"stem":1865,"__hash__":1866},"docs\u002Fspatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002Fcheck-line-network-connectivity-pyqgis\u002Findex.md","Check Line Network Connectivity in PyQGIS",{"type":7,"value":8,"toc":1843},"minimark",[9,13,23,31,136,141,154,158,161,609,616,620,623,697,1067,1076,1080,1083,1269,1282,1286,1289,1350,1585,1595,1599,1602,1696,1706,1710,1725,1729,1755,1759,1762,1766,1780,1786,1800,1806,1810,1839],[10,11,5],"h1",{"id":12},"check-line-network-connectivity-in-pyqgis",[14,15,16,17,22],"p",{},"A line layer can look perfectly connected on screen and be broken for every algorithm that walks it. A road that stops 30 cm short of a junction, a pipe that crosses another without a shared vertex, a river segment digitized in isolation — each splits the network into pieces, and a shortest-path or upstream trace simply stops at the break. These errors are invisible at normal zoom and fatal to analysis, which makes a connectivity check one of the most valuable things to run before any ",[18,19,21],"a",{"href":20},"\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002F","network analysis",".",[14,24,25,26,30],{},"This recipe belongs to ",[18,27,29],{"href":28},"\u002Fspatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002F","Data Quality & Topology Validation",". It finds dangling line ends, separates genuine dead ends from undershoots, detects crossings without nodes, and counts the network's connected components.",[14,32,33],{},[34,35,40,44,48,55,64,72,77,83,89,93,96,98,102,108,110,115,118,125,129,133],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 266","img","Four network errors: an undershoot short of a junction, an overshoot past it, a crossing without a node, and an isolated group of lines","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"Four connectivity errors",[45,46,47],"desc",{},"An undershoot: a line ending a short distance before another line it should meet. An overshoot: a line extending slightly past the line it meets, leaving a short dangling stub. A missing node: two lines crossing without a shared vertex, so a router cannot turn there. A disconnected piece: a group of lines touching each other but nothing else in the network.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","266","#f6f3ea",[56,57,63],"text",{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"380","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","Looks connected, is not",[65,66],"line",{"x1":67,"y1":68,"x2":69,"y2":68,"stroke":70,"style":71},"40","150","170","#2563eb","stroke-width:3",[65,73],{"x1":74,"y1":75,"x2":74,"y2":76,"stroke":70,"style":71},"186","80","220",[78,79],"circle",{"cx":69,"cy":68,"r":80,"fill":81,"stroke":81,"style":82},"5","#b91c1c","stroke-width:1",[56,84,88],{"x":85,"y":86,"style":87,"fill":81,"textAnchor":62},"112","246","text-anchor:middle;font-size:11.0px;font-family:sans-serif","undershoot",[65,90],{"x1":91,"y1":68,"x2":92,"y2":68,"stroke":70,"style":71},"230","372",[65,94],{"x1":95,"y1":75,"x2":95,"y2":76,"stroke":70,"style":71},"350",[78,97],{"cx":92,"cy":68,"r":80,"fill":81,"stroke":81,"style":82},[56,99,101],{"x":100,"y":86,"style":87,"fill":81,"textAnchor":62},"300","overshoot",[65,103],{"x1":104,"y1":105,"x2":106,"y2":107,"stroke":70,"style":71},"410","90","540","210",[65,109],{"x1":104,"y1":107,"x2":106,"y2":105,"stroke":70,"style":71},[78,111],{"cx":112,"cy":68,"r":113,"fill":54,"stroke":81,"style":114},"475","7","stroke-width:2",[56,116,117],{"x":112,"y":86,"style":87,"fill":81,"textAnchor":62},"no node at crossing",[65,119],{"x1":120,"y1":121,"x2":122,"y2":123,"stroke":124,"style":71},"590","110","650","100","#59645f",[65,126],{"x1":122,"y1":123,"x2":127,"y2":128,"stroke":124,"style":71},"700","160",[65,130],{"x1":122,"y1":123,"x2":131,"y2":132,"stroke":124,"style":71},"690","70",[56,134,135],{"x":122,"y":86,"style":87,"fill":81,"textAnchor":62},"isolated piece",[137,138,140],"h2",{"id":139},"prerequisites","Prerequisites",[142,143,144,148,151],"ul",{},[145,146,147],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series.",[145,149,150],{},"A line layer in a projected CRS, so tolerances are in metres.",[145,152,153],{},"A tolerance that reflects the data's accuracy — typically 0.5 to 2 metres for roads digitized from imagery, a few centimetres for survey-grade utility data.",[137,155,157],{"id":156},"find-dangling-ends","Find dangling ends",[14,159,160],{},"An end point that touches no other line is a dangle. Every network has some legitimately — cul-de-sacs, river sources, the stub of a service pipe — so the first step is simply to find them all.",[162,163,168],"pre",{"className":164,"code":165,"language":166,"meta":167,"style":167},"language-python shiki shiki-themes github-dark","from collections import Counter\nfrom qgis.core import (QgsProject, QgsPointXY, QgsVectorLayer, QgsFeature,\n                       QgsGeometry, QgsSpatialIndex)\n\nroads = QgsProject.instance().mapLayersByName(\"roads\")[0]\nPREC = 3   # round coordinates to millimetres for matching\n\ndef key(pt):\n    return (round(pt.x(), PREC), round(pt.y(), PREC))\n\nendpoint_count = Counter()\nends = []\nfor f in roads.getFeatures():\n    for part in f.geometry().constParts():\n        a, b = part.startPoint(), part.endPoint()\n        for p in (a, b):\n            endpoint_count[key(p)] += 1\n            ends.append((f.id(), QgsPointXY(p.x(), p.y())))\n\nindex = QgsSpatialIndex(roads.getFeatures(),\n                        flags=QgsSpatialIndex.FlagStoreFeatureGeometries)\ngeoms = {f.id(): f.geometry() for f in roads.getFeatures()}\n\ndangles = []\nfor fid, pt in ends:\n    if endpoint_count[key(pt)] > 1:\n        continue                                    # shared with another end\n    g = QgsGeometry.fromPointXY(pt)\n    touching = [o for o in index.intersects(g.boundingBox().buffered(0.001))\n                if o != fid and geoms[o].distance(g) \u003C 0.001]\n    if not touching:\n        dangles.append((fid, pt))\nprint(len(dangles), \"dangling ends\")\n","python","",[169,170,171,189,202,208,215,240,256,261,274,304,309,320,331,346,360,371,385,397,403,408,419,431,451,456,466,479,497,506,517,543,571,582,588],"code",{"__ignoreMap":167},[172,173,175,179,183,186],"span",{"class":65,"line":174},1,[172,176,178],{"class":177},"snl16","from",[172,180,182],{"class":181},"s95oV"," collections ",[172,184,185],{"class":177},"import",[172,187,188],{"class":181}," Counter\n",[172,190,192,194,197,199],{"class":65,"line":191},2,[172,193,178],{"class":177},[172,195,196],{"class":181}," qgis.core ",[172,198,185],{"class":177},[172,200,201],{"class":181}," (QgsProject, QgsPointXY, QgsVectorLayer, QgsFeature,\n",[172,203,205],{"class":65,"line":204},3,[172,206,207],{"class":181},"                       QgsGeometry, QgsSpatialIndex)\n",[172,209,211],{"class":65,"line":210},4,[172,212,214],{"emptyLinePlaceholder":213},true,"\n",[172,216,218,221,224,227,231,234,237],{"class":65,"line":217},5,[172,219,220],{"class":181},"roads ",[172,222,223],{"class":177},"=",[172,225,226],{"class":181}," QgsProject.instance().mapLayersByName(",[172,228,230],{"class":229},"sU2Wk","\"roads\"",[172,232,233],{"class":181},")[",[172,235,51],{"class":236},"sDLfK",[172,238,239],{"class":181},"]\n",[172,241,243,246,249,252],{"class":65,"line":242},6,[172,244,245],{"class":236},"PREC",[172,247,248],{"class":177}," =",[172,250,251],{"class":236}," 3",[172,253,255],{"class":254},"sjoCn","   # round coordinates to millimetres for matching\n",[172,257,259],{"class":65,"line":258},7,[172,260,214],{"emptyLinePlaceholder":213},[172,262,264,267,271],{"class":65,"line":263},8,[172,265,266],{"class":177},"def",[172,268,270],{"class":269},"svObZ"," key",[172,272,273],{"class":181},"(pt):\n",[172,275,277,280,283,286,289,291,294,296,299,301],{"class":65,"line":276},9,[172,278,279],{"class":177},"    return",[172,281,282],{"class":181}," (",[172,284,285],{"class":236},"round",[172,287,288],{"class":181},"(pt.x(), ",[172,290,245],{"class":236},[172,292,293],{"class":181},"), ",[172,295,285],{"class":236},[172,297,298],{"class":181},"(pt.y(), ",[172,300,245],{"class":236},[172,302,303],{"class":181},"))\n",[172,305,307],{"class":65,"line":306},10,[172,308,214],{"emptyLinePlaceholder":213},[172,310,312,315,317],{"class":65,"line":311},11,[172,313,314],{"class":181},"endpoint_count ",[172,316,223],{"class":177},[172,318,319],{"class":181}," Counter()\n",[172,321,323,326,328],{"class":65,"line":322},12,[172,324,325],{"class":181},"ends ",[172,327,223],{"class":177},[172,329,330],{"class":181}," []\n",[172,332,334,337,340,343],{"class":65,"line":333},13,[172,335,336],{"class":177},"for",[172,338,339],{"class":181}," f ",[172,341,342],{"class":177},"in",[172,344,345],{"class":181}," roads.getFeatures():\n",[172,347,349,352,355,357],{"class":65,"line":348},14,[172,350,351],{"class":177},"    for",[172,353,354],{"class":181}," part ",[172,356,342],{"class":177},[172,358,359],{"class":181}," f.geometry().constParts():\n",[172,361,363,366,368],{"class":65,"line":362},15,[172,364,365],{"class":181},"        a, b ",[172,367,223],{"class":177},[172,369,370],{"class":181}," part.startPoint(), part.endPoint()\n",[172,372,374,377,380,382],{"class":65,"line":373},16,[172,375,376],{"class":177},"        for",[172,378,379],{"class":181}," p ",[172,381,342],{"class":177},[172,383,384],{"class":181}," (a, b):\n",[172,386,388,391,394],{"class":65,"line":387},17,[172,389,390],{"class":181},"            endpoint_count[key(p)] ",[172,392,393],{"class":177},"+=",[172,395,396],{"class":236}," 1\n",[172,398,400],{"class":65,"line":399},18,[172,401,402],{"class":181},"            ends.append((f.id(), QgsPointXY(p.x(), p.y())))\n",[172,404,406],{"class":65,"line":405},19,[172,407,214],{"emptyLinePlaceholder":213},[172,409,411,414,416],{"class":65,"line":410},20,[172,412,413],{"class":181},"index ",[172,415,223],{"class":177},[172,417,418],{"class":181}," QgsSpatialIndex(roads.getFeatures(),\n",[172,420,422,426,428],{"class":65,"line":421},21,[172,423,425],{"class":424},"s9osk","                        flags",[172,427,223],{"class":177},[172,429,430],{"class":181},"QgsSpatialIndex.FlagStoreFeatureGeometries)\n",[172,432,434,437,439,442,444,446,448],{"class":65,"line":433},22,[172,435,436],{"class":181},"geoms ",[172,438,223],{"class":177},[172,440,441],{"class":181}," {f.id(): f.geometry() ",[172,443,336],{"class":177},[172,445,339],{"class":181},[172,447,342],{"class":177},[172,449,450],{"class":181}," roads.getFeatures()}\n",[172,452,454],{"class":65,"line":453},23,[172,455,214],{"emptyLinePlaceholder":213},[172,457,459,462,464],{"class":65,"line":458},24,[172,460,461],{"class":181},"dangles ",[172,463,223],{"class":177},[172,465,330],{"class":181},[172,467,469,471,474,476],{"class":65,"line":468},25,[172,470,336],{"class":177},[172,472,473],{"class":181}," fid, pt ",[172,475,342],{"class":177},[172,477,478],{"class":181}," ends:\n",[172,480,482,485,488,491,494],{"class":65,"line":481},26,[172,483,484],{"class":177},"    if",[172,486,487],{"class":181}," endpoint_count[key(pt)] ",[172,489,490],{"class":177},">",[172,492,493],{"class":236}," 1",[172,495,496],{"class":181},":\n",[172,498,500,503],{"class":65,"line":499},27,[172,501,502],{"class":177},"        continue",[172,504,505],{"class":254},"                                    # shared with another end\n",[172,507,509,512,514],{"class":65,"line":508},28,[172,510,511],{"class":181},"    g ",[172,513,223],{"class":177},[172,515,516],{"class":181}," QgsGeometry.fromPointXY(pt)\n",[172,518,520,523,525,528,530,533,535,538,541],{"class":65,"line":519},29,[172,521,522],{"class":181},"    touching ",[172,524,223],{"class":177},[172,526,527],{"class":181}," [o ",[172,529,336],{"class":177},[172,531,532],{"class":181}," o ",[172,534,342],{"class":177},[172,536,537],{"class":181}," index.intersects(g.boundingBox().buffered(",[172,539,540],{"class":236},"0.001",[172,542,303],{"class":181},[172,544,546,549,551,554,557,560,563,566,569],{"class":65,"line":545},30,[172,547,548],{"class":177},"                if",[172,550,532],{"class":181},[172,552,553],{"class":177},"!=",[172,555,556],{"class":181}," fid ",[172,558,559],{"class":177},"and",[172,561,562],{"class":181}," geoms[o].distance(g) ",[172,564,565],{"class":177},"\u003C",[172,567,568],{"class":236}," 0.001",[172,570,239],{"class":181},[172,572,574,576,579],{"class":65,"line":573},31,[172,575,484],{"class":177},[172,577,578],{"class":177}," not",[172,580,581],{"class":181}," touching:\n",[172,583,585],{"class":65,"line":584},32,[172,586,587],{"class":181},"        dangles.append((fid, pt))\n",[172,589,591,594,597,600,603,606],{"class":65,"line":590},33,[172,592,593],{"class":236},"print",[172,595,596],{"class":181},"(",[172,598,599],{"class":236},"len",[172,601,602],{"class":181},"(dangles), ",[172,604,605],{"class":229},"\"dangling ends\"",[172,607,608],{"class":181},")\n",[14,610,611,615],{},[612,613,614],"strong",{},"Breakdown:"," Counting rounded end coordinates finds ends shared with another line's end in one pass; rounding to millimetres absorbs floating-point noise without merging genuinely separate points. An end not shared with another end may still touch the middle of another line — a T-junction where the side road ends on the main road. The spatial index check catches those, so only ends that touch nothing at all count as dangles. Lines that are multipart are handled part by part, because each part has its own ends.",[137,617,619],{"id":618},"tell-undershoots-from-real-dead-ends","Tell undershoots from real dead ends",[14,621,622],{},"A dangle within a short distance of another line is probably an undershoot: the digitizer meant to connect it. A dangle with nothing nearby is probably a real dead end.",[14,624,625],{},[34,626,629,632,635,638,641,648,654,660,664,669,673,676,679,683,687,690,693],{"viewBox":627,"role":37,"ariaLabel":628,"xmlns":39},"0 0 760 250","Dangling ends classed by distance to the nearest line: within tolerance an undershoot, beyond it a dead end, and short lines dangling at both ends as stubs",[41,630,631],{},"Classifying dangles by distance",[45,633,634],{},"Each dangling end is measured to the nearest other line. Within the snap tolerance, for example two metres, it is classed as a probable undershoot and listed for snapping. Beyond the tolerance it is classed as a probable dead end, such as a cul-de-sac. Very short lines with a dangle at both ends are classed as stubs, often left over from editing.",[49,636],{"x":51,"y":51,"width":52,"height":637,"fill":54},"250",[56,639,640],{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"Distance to the nearest line decides",[49,642],{"x":643,"y":644,"width":645,"height":68,"rx":646,"fill":647,"stroke":81,"style":114},"24","60","222","8","#fdf2e2",[56,649,653],{"x":650,"y":651,"style":652,"fill":81,"textAnchor":62},"135","110.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","≤ tolerance",[56,655,659],{"x":650,"y":656,"style":657,"fill":658,"textAnchor":62},"138.78","text-anchor:middle;font-size:10.5px;font-family:sans-serif","#2f3b35","probable undershoot",[56,661,663],{"x":650,"y":662,"style":657,"fill":124,"textAnchor":62},"166.78","snap to the line",[49,665],{"x":666,"y":644,"width":645,"height":68,"rx":646,"fill":667,"stroke":668,"style":114},"269","#e8efe6","#15803d",[56,670,672],{"x":58,"y":651,"style":652,"fill":671,"textAnchor":62},"#166534","> tolerance",[56,674,675],{"x":58,"y":656,"style":657,"fill":658,"textAnchor":62},"probable dead end",[56,677,678],{"x":58,"y":662,"style":657,"fill":124,"textAnchor":62},"cul-de-sac, source",[49,680],{"x":681,"y":644,"width":645,"height":68,"rx":646,"fill":647,"stroke":682,"style":114},"514","#b45309",[56,684,686],{"x":685,"y":651,"style":652,"fill":682,"textAnchor":62},"625","both ends dangle",[56,688,689],{"x":685,"y":656,"style":657,"fill":658,"textAnchor":62},"short stub",[56,691,692],{"x":685,"y":662,"style":657,"fill":124,"textAnchor":62},"editing leftover",[56,694,696],{"x":58,"y":695,"style":657,"fill":124,"textAnchor":62},"236","tolerance from data accuracy, e.g. 2 m for imagery digitizing",[162,698,700],{"className":164,"code":699,"language":166,"meta":167,"style":167},"TOL = 2.0   # metres\n\ndangle_layer = QgsVectorLayer(\n    f\"Point?crs={roads.crs().authid()}&field=road_fid:integer\"\n    \"&field=gap_m:double&field=kind:string(12)\", \"dangles\", \"memory\")\nrows = []\ndangle_count_per_line = Counter(fid for fid, _ in dangles)\nfor fid, pt in dangles:\n    g = QgsGeometry.fromPointXY(pt)\n    near = [o for o in index.nearestNeighbor(pt, 3, TOL * 5) if o != fid]\n    gap = min((geoms[o].distance(g) for o in near), default=float(\"inf\"))\n    if dangle_count_per_line[fid] == 2 and geoms[fid].length() \u003C TOL * 5:\n        kind = \"stub\"\n    elif gap \u003C= TOL:\n        kind = \"undershoot\"\n    else:\n        kind = \"dead_end\"\n    f = QgsFeature(dangle_layer.fields())\n    f.setAttributes([fid, None if gap == float(\"inf\") else round(gap, 3), kind])\n    f.setGeometry(g)\n    rows.append(f)\ndangle_layer.dataProvider().addFeatures(rows)\nQgsProject.instance().addMapLayer(dangle_layer)\nprint(Counter(r[\"kind\"] for r in rows))\n",[169,701,702,715,719,729,749,767,776,796,807,815,859,896,926,936,951,960,967,976,986,1024,1029,1034,1039,1044],{"__ignoreMap":167},[172,703,704,707,709,712],{"class":65,"line":174},[172,705,706],{"class":236},"TOL",[172,708,248],{"class":177},[172,710,711],{"class":236}," 2.0",[172,713,714],{"class":254},"   # metres\n",[172,716,717],{"class":65,"line":191},[172,718,214],{"emptyLinePlaceholder":213},[172,720,721,724,726],{"class":65,"line":204},[172,722,723],{"class":181},"dangle_layer ",[172,725,223],{"class":177},[172,727,728],{"class":181}," QgsVectorLayer(\n",[172,730,731,734,737,740,743,746],{"class":65,"line":210},[172,732,733],{"class":177},"    f",[172,735,736],{"class":229},"\"Point?crs=",[172,738,739],{"class":236},"{",[172,741,742],{"class":181},"roads.crs().authid()",[172,744,745],{"class":236},"}",[172,747,748],{"class":229},"&field=road_fid:integer\"\n",[172,750,751,754,757,760,762,765],{"class":65,"line":217},[172,752,753],{"class":229},"    \"&field=gap_m:double&field=kind:string(12)\"",[172,755,756],{"class":181},", ",[172,758,759],{"class":229},"\"dangles\"",[172,761,756],{"class":181},[172,763,764],{"class":229},"\"memory\"",[172,766,608],{"class":181},[172,768,769,772,774],{"class":65,"line":242},[172,770,771],{"class":181},"rows ",[172,773,223],{"class":177},[172,775,330],{"class":181},[172,777,778,781,783,786,788,791,793],{"class":65,"line":258},[172,779,780],{"class":181},"dangle_count_per_line ",[172,782,223],{"class":177},[172,784,785],{"class":181}," Counter(fid ",[172,787,336],{"class":177},[172,789,790],{"class":181}," fid, _ ",[172,792,342],{"class":177},[172,794,795],{"class":181}," dangles)\n",[172,797,798,800,802,804],{"class":65,"line":263},[172,799,336],{"class":177},[172,801,473],{"class":181},[172,803,342],{"class":177},[172,805,806],{"class":181}," dangles:\n",[172,808,809,811,813],{"class":65,"line":276},[172,810,511],{"class":181},[172,812,223],{"class":177},[172,814,516],{"class":181},[172,816,817,820,822,824,826,828,830,833,836,838,840,843,846,849,852,854,856],{"class":65,"line":306},[172,818,819],{"class":181},"    near ",[172,821,223],{"class":177},[172,823,527],{"class":181},[172,825,336],{"class":177},[172,827,532],{"class":181},[172,829,342],{"class":177},[172,831,832],{"class":181}," index.nearestNeighbor(pt, ",[172,834,835],{"class":236},"3",[172,837,756],{"class":181},[172,839,706],{"class":236},[172,841,842],{"class":177}," *",[172,844,845],{"class":236}," 5",[172,847,848],{"class":181},") ",[172,850,851],{"class":177},"if",[172,853,532],{"class":181},[172,855,553],{"class":177},[172,857,858],{"class":181}," fid]\n",[172,860,861,864,866,869,872,874,876,878,881,884,886,889,891,894],{"class":65,"line":311},[172,862,863],{"class":181},"    gap ",[172,865,223],{"class":177},[172,867,868],{"class":236}," min",[172,870,871],{"class":181},"((geoms[o].distance(g) ",[172,873,336],{"class":177},[172,875,532],{"class":181},[172,877,342],{"class":177},[172,879,880],{"class":181}," near), ",[172,882,883],{"class":424},"default",[172,885,223],{"class":177},[172,887,888],{"class":236},"float",[172,890,596],{"class":181},[172,892,893],{"class":229},"\"inf\"",[172,895,303],{"class":181},[172,897,898,900,903,906,909,912,915,917,920,922,924],{"class":65,"line":322},[172,899,484],{"class":177},[172,901,902],{"class":181}," dangle_count_per_line[fid] ",[172,904,905],{"class":177},"==",[172,907,908],{"class":236}," 2",[172,910,911],{"class":177}," and",[172,913,914],{"class":181}," geoms[fid].length() ",[172,916,565],{"class":177},[172,918,919],{"class":236}," TOL",[172,921,842],{"class":177},[172,923,845],{"class":236},[172,925,496],{"class":181},[172,927,928,931,933],{"class":65,"line":333},[172,929,930],{"class":181},"        kind ",[172,932,223],{"class":177},[172,934,935],{"class":229}," \"stub\"\n",[172,937,938,941,944,947,949],{"class":65,"line":348},[172,939,940],{"class":177},"    elif",[172,942,943],{"class":181}," gap ",[172,945,946],{"class":177},"\u003C=",[172,948,919],{"class":236},[172,950,496],{"class":181},[172,952,953,955,957],{"class":65,"line":362},[172,954,930],{"class":181},[172,956,223],{"class":177},[172,958,959],{"class":229}," \"undershoot\"\n",[172,961,962,965],{"class":65,"line":373},[172,963,964],{"class":177},"    else",[172,966,496],{"class":181},[172,968,969,971,973],{"class":65,"line":387},[172,970,930],{"class":181},[172,972,223],{"class":177},[172,974,975],{"class":229}," \"dead_end\"\n",[172,977,978,981,983],{"class":65,"line":399},[172,979,980],{"class":181},"    f ",[172,982,223],{"class":177},[172,984,985],{"class":181}," QgsFeature(dangle_layer.fields())\n",[172,987,988,991,994,997,999,1001,1004,1006,1008,1010,1013,1016,1019,1021],{"class":65,"line":405},[172,989,990],{"class":181},"    f.setAttributes([fid, ",[172,992,993],{"class":236},"None",[172,995,996],{"class":177}," if",[172,998,943],{"class":181},[172,1000,905],{"class":177},[172,1002,1003],{"class":236}," float",[172,1005,596],{"class":181},[172,1007,893],{"class":229},[172,1009,848],{"class":181},[172,1011,1012],{"class":177},"else",[172,1014,1015],{"class":236}," round",[172,1017,1018],{"class":181},"(gap, ",[172,1020,835],{"class":236},[172,1022,1023],{"class":181},"), kind])\n",[172,1025,1026],{"class":65,"line":410},[172,1027,1028],{"class":181},"    f.setGeometry(g)\n",[172,1030,1031],{"class":65,"line":421},[172,1032,1033],{"class":181},"    rows.append(f)\n",[172,1035,1036],{"class":65,"line":433},[172,1037,1038],{"class":181},"dangle_layer.dataProvider().addFeatures(rows)\n",[172,1040,1041],{"class":65,"line":453},[172,1042,1043],{"class":181},"QgsProject.instance().addMapLayer(dangle_layer)\n",[172,1045,1046,1048,1051,1054,1057,1059,1062,1064],{"class":65,"line":458},[172,1047,593],{"class":236},[172,1049,1050],{"class":181},"(Counter(r[",[172,1052,1053],{"class":229},"\"kind\"",[172,1055,1056],{"class":181},"] ",[172,1058,336],{"class":177},[172,1060,1061],{"class":181}," r ",[172,1063,342],{"class":177},[172,1065,1066],{"class":181}," rows))\n",[14,1068,1069,1071,1072,1075],{},[612,1070,614],{}," ",[169,1073,1074],{},"nearestNeighbor(point, k, maxDistance)"," returns candidate ids within a search radius, which keeps the search local on large networks. Recording the gap distance on each point lets you style undershoots by how far off they are and check the tolerance choice: a histogram of gaps for a well-digitized network shows a pile of tiny values (undershoots) and a sparse spread of larger ones (real dead ends), with a natural threshold between them. Short lines dangling at both ends are usually leftovers from splitting or deleting and are best reviewed separately.",[137,1077,1079],{"id":1078},"overshoots-and-missing-nodes-at-crossings","Overshoots and missing nodes at crossings",[14,1081,1082],{},"Overshoots leave a short stub past a junction; crossings without a shared vertex mean two lines intersect but the network has no node there. Both are found by intersecting lines with each other.",[162,1084,1086],{"className":164,"code":1085,"language":166,"meta":167,"style":167},"crossings = []\nfor fid, geom in geoms.items():\n    for other in index.intersects(geom.boundingBox()):\n        if other \u003C= fid:\n            continue\n        inter = geom.intersection(geoms[other])\n        if inter.isEmpty():\n            continue\n        for v in inter.vertices():\n            p = QgsPointXY(v.x(), v.y())\n            on_vertex_a = any(key(x) == key(p) for x in geom.vertices())\n            on_vertex_b = any(key(x) == key(p) for x in geoms[other].vertices())\n            if not (on_vertex_a and on_vertex_b):\n                crossings.append((fid, other, p))\nprint(len(crossings), \"intersections without a shared vertex\")\n",[169,1087,1088,1097,1109,1121,1133,1138,1148,1155,1159,1171,1181,1209,1233,1248,1253],{"__ignoreMap":167},[172,1089,1090,1093,1095],{"class":65,"line":174},[172,1091,1092],{"class":181},"crossings ",[172,1094,223],{"class":177},[172,1096,330],{"class":181},[172,1098,1099,1101,1104,1106],{"class":65,"line":191},[172,1100,336],{"class":177},[172,1102,1103],{"class":181}," fid, geom ",[172,1105,342],{"class":177},[172,1107,1108],{"class":181}," geoms.items():\n",[172,1110,1111,1113,1116,1118],{"class":65,"line":204},[172,1112,351],{"class":177},[172,1114,1115],{"class":181}," other ",[172,1117,342],{"class":177},[172,1119,1120],{"class":181}," index.intersects(geom.boundingBox()):\n",[172,1122,1123,1126,1128,1130],{"class":65,"line":210},[172,1124,1125],{"class":177},"        if",[172,1127,1115],{"class":181},[172,1129,946],{"class":177},[172,1131,1132],{"class":181}," fid:\n",[172,1134,1135],{"class":65,"line":217},[172,1136,1137],{"class":177},"            continue\n",[172,1139,1140,1143,1145],{"class":65,"line":242},[172,1141,1142],{"class":181},"        inter ",[172,1144,223],{"class":177},[172,1146,1147],{"class":181}," geom.intersection(geoms[other])\n",[172,1149,1150,1152],{"class":65,"line":258},[172,1151,1125],{"class":177},[172,1153,1154],{"class":181}," inter.isEmpty():\n",[172,1156,1157],{"class":65,"line":263},[172,1158,1137],{"class":177},[172,1160,1161,1163,1166,1168],{"class":65,"line":276},[172,1162,376],{"class":177},[172,1164,1165],{"class":181}," v ",[172,1167,342],{"class":177},[172,1169,1170],{"class":181}," inter.vertices():\n",[172,1172,1173,1176,1178],{"class":65,"line":306},[172,1174,1175],{"class":181},"            p ",[172,1177,223],{"class":177},[172,1179,1180],{"class":181}," QgsPointXY(v.x(), v.y())\n",[172,1182,1183,1186,1188,1191,1194,1196,1199,1201,1204,1206],{"class":65,"line":311},[172,1184,1185],{"class":181},"            on_vertex_a ",[172,1187,223],{"class":177},[172,1189,1190],{"class":236}," any",[172,1192,1193],{"class":181},"(key(x) ",[172,1195,905],{"class":177},[172,1197,1198],{"class":181}," key(p) ",[172,1200,336],{"class":177},[172,1202,1203],{"class":181}," x ",[172,1205,342],{"class":177},[172,1207,1208],{"class":181}," geom.vertices())\n",[172,1210,1211,1214,1216,1218,1220,1222,1224,1226,1228,1230],{"class":65,"line":322},[172,1212,1213],{"class":181},"            on_vertex_b ",[172,1215,223],{"class":177},[172,1217,1190],{"class":236},[172,1219,1193],{"class":181},[172,1221,905],{"class":177},[172,1223,1198],{"class":181},[172,1225,336],{"class":177},[172,1227,1203],{"class":181},[172,1229,342],{"class":177},[172,1231,1232],{"class":181}," geoms[other].vertices())\n",[172,1234,1235,1238,1240,1243,1245],{"class":65,"line":333},[172,1236,1237],{"class":177},"            if",[172,1239,578],{"class":177},[172,1241,1242],{"class":181}," (on_vertex_a ",[172,1244,559],{"class":177},[172,1246,1247],{"class":181}," on_vertex_b):\n",[172,1249,1250],{"class":65,"line":348},[172,1251,1252],{"class":181},"                crossings.append((fid, other, p))\n",[172,1254,1255,1257,1259,1261,1264,1267],{"class":65,"line":362},[172,1256,593],{"class":236},[172,1258,596],{"class":181},[172,1260,599],{"class":236},[172,1262,1263],{"class":181},"(crossings), ",[172,1265,1266],{"class":229},"\"intersections without a shared vertex\"",[172,1268,608],{"class":181},[14,1270,1271,1273,1274,1277,1278,1281],{},[612,1272,614],{}," Two lines that should connect at a crossing must share a vertex there; if either line lacks a vertex at the intersection point, a graph builder will not create a node, and routes cannot turn. Not every unnoded crossing is an error — a bridge over a road should not connect to it — which is why networks often carry a level or z-order attribute. Filter the results by those attributes before fixing anything. An overshoot appears in this list as a crossing near a line's end; combine the two checks to label it. ",[169,1275,1276],{},"native:splitwithlines"," or ",[169,1279,1280],{},"native:lineintersections"," followed by splitting can insert the missing nodes in bulk.",[137,1283,1285],{"id":1284},"count-connected-components","Count connected components",[14,1287,1288],{},"The decisive question is whether the network is one piece. Building a graph from line end points and counting connected components answers it and shows where the pieces are.",[14,1290,1291],{},[34,1292,1295,1298,1301,1304,1307,1312,1314,1316,1318,1322,1324,1327,1330,1334,1337,1342,1346],{"viewBox":1293,"role":37,"ariaLabel":1294,"xmlns":39},"0 0 760 240","Network lines grouped into connected components: one main component and a few small isolated pieces",[41,1296,1297],{},"Components of a network",[45,1299,1300],{},"Lines become edges between their end nodes. A union-find over all edges groups nodes into components. A healthy road network has one large component holding nearly all lines, plus a few small ones such as private estates or ferry-only islands. Many small components, or a main component holding only part of the network, point to systematic connectivity errors.",[49,1302],{"x":51,"y":51,"width":52,"height":1303,"fill":54},"240",[56,1305,1306],{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"One big piece, a few small ones",[65,1308],{"x1":644,"y1":1309,"x2":1310,"y2":105,"stroke":1311,"style":71},"120","140","#0f766e",[65,1313],{"x1":1310,"y1":105,"x2":76,"y2":1309,"stroke":1311,"style":71},[65,1315],{"x1":76,"y1":1309,"x2":100,"y2":123,"stroke":1311,"style":71},[65,1317],{"x1":1310,"y1":105,"x2":128,"y2":69,"stroke":1311,"style":71},[65,1319],{"x1":76,"y1":1309,"x2":1320,"y2":1321,"stroke":1311,"style":71},"260","190",[65,1323],{"x1":100,"y1":123,"x2":58,"y2":1310,"stroke":1311,"style":71},[65,1325],{"x1":58,"y1":1310,"x2":1326,"y2":75,"stroke":1311,"style":71},"420",[56,1328,1329],{"x":1303,"y":645,"style":87,"fill":1311,"textAnchor":62},"main component — 98 % of lines",[65,1331],{"x1":1332,"y1":123,"x2":1333,"y2":105,"stroke":682,"style":71},"520","580",[65,1335],{"x1":1333,"y1":105,"x2":1336,"y2":1310,"stroke":682,"style":71},"610",[56,1338,1341],{"x":1339,"y":1340,"style":87,"fill":682,"textAnchor":62},"570","180","small piece",[65,1343],{"x1":1344,"y1":1309,"x2":1345,"y2":68,"stroke":81,"style":71},"660","720",[56,1347,1349],{"x":131,"y":1348,"style":87,"fill":81,"textAnchor":62},"200","isolated line",[162,1351,1353],{"className":164,"code":1352,"language":166,"meta":167,"style":167},"parent = {}\ndef find(x):\n    parent.setdefault(x, x)\n    while parent[x] != x:\n        parent[x] = parent[parent[x]]\n        x = parent[x]\n    return x\n\nfor f in roads.getFeatures():\n    for part in f.geometry().constParts():\n        a, b = key(part.startPoint()), key(part.endPoint())\n        parent[find(a)] = find(b)\n\ncomponent_of_line = {}\nfor f in roads.getFeatures():\n    part = next(f.geometry().constParts())\n    component_of_line[f.id()] = find(key(part.startPoint()))\nsizes = Counter(component_of_line.values())\nmain, main_size = sizes.most_common(1)[0]\nprint(f\"{len(sizes)} components; main holds {main_size \u002F len(component_of_line):.1%} of lines\")\n",[169,1354,1355,1365,1375,1380,1393,1403,1413,1420,1424,1434,1444,1453,1463,1467,1476,1486,1499,1509,1519,1538],{"__ignoreMap":167},[172,1356,1357,1360,1362],{"class":65,"line":174},[172,1358,1359],{"class":181},"parent ",[172,1361,223],{"class":177},[172,1363,1364],{"class":181}," {}\n",[172,1366,1367,1369,1372],{"class":65,"line":191},[172,1368,266],{"class":177},[172,1370,1371],{"class":269}," find",[172,1373,1374],{"class":181},"(x):\n",[172,1376,1377],{"class":65,"line":204},[172,1378,1379],{"class":181},"    parent.setdefault(x, x)\n",[172,1381,1382,1385,1388,1390],{"class":65,"line":210},[172,1383,1384],{"class":177},"    while",[172,1386,1387],{"class":181}," parent[x] ",[172,1389,553],{"class":177},[172,1391,1392],{"class":181}," x:\n",[172,1394,1395,1398,1400],{"class":65,"line":217},[172,1396,1397],{"class":181},"        parent[x] ",[172,1399,223],{"class":177},[172,1401,1402],{"class":181}," parent[parent[x]]\n",[172,1404,1405,1408,1410],{"class":65,"line":242},[172,1406,1407],{"class":181},"        x ",[172,1409,223],{"class":177},[172,1411,1412],{"class":181}," parent[x]\n",[172,1414,1415,1417],{"class":65,"line":258},[172,1416,279],{"class":177},[172,1418,1419],{"class":181}," x\n",[172,1421,1422],{"class":65,"line":263},[172,1423,214],{"emptyLinePlaceholder":213},[172,1425,1426,1428,1430,1432],{"class":65,"line":276},[172,1427,336],{"class":177},[172,1429,339],{"class":181},[172,1431,342],{"class":177},[172,1433,345],{"class":181},[172,1435,1436,1438,1440,1442],{"class":65,"line":306},[172,1437,351],{"class":177},[172,1439,354],{"class":181},[172,1441,342],{"class":177},[172,1443,359],{"class":181},[172,1445,1446,1448,1450],{"class":65,"line":311},[172,1447,365],{"class":181},[172,1449,223],{"class":177},[172,1451,1452],{"class":181}," key(part.startPoint()), key(part.endPoint())\n",[172,1454,1455,1458,1460],{"class":65,"line":322},[172,1456,1457],{"class":181},"        parent[find(a)] ",[172,1459,223],{"class":177},[172,1461,1462],{"class":181}," find(b)\n",[172,1464,1465],{"class":65,"line":333},[172,1466,214],{"emptyLinePlaceholder":213},[172,1468,1469,1472,1474],{"class":65,"line":348},[172,1470,1471],{"class":181},"component_of_line ",[172,1473,223],{"class":177},[172,1475,1364],{"class":181},[172,1477,1478,1480,1482,1484],{"class":65,"line":362},[172,1479,336],{"class":177},[172,1481,339],{"class":181},[172,1483,342],{"class":177},[172,1485,345],{"class":181},[172,1487,1488,1491,1493,1496],{"class":65,"line":373},[172,1489,1490],{"class":181},"    part ",[172,1492,223],{"class":177},[172,1494,1495],{"class":236}," next",[172,1497,1498],{"class":181},"(f.geometry().constParts())\n",[172,1500,1501,1504,1506],{"class":65,"line":387},[172,1502,1503],{"class":181},"    component_of_line[f.id()] ",[172,1505,223],{"class":177},[172,1507,1508],{"class":181}," find(key(part.startPoint()))\n",[172,1510,1511,1514,1516],{"class":65,"line":399},[172,1512,1513],{"class":181},"sizes ",[172,1515,223],{"class":177},[172,1517,1518],{"class":181}," Counter(component_of_line.values())\n",[172,1520,1521,1524,1526,1529,1532,1534,1536],{"class":65,"line":405},[172,1522,1523],{"class":181},"main, main_size ",[172,1525,223],{"class":177},[172,1527,1528],{"class":181}," sizes.most_common(",[172,1530,1531],{"class":236},"1",[172,1533,233],{"class":181},[172,1535,51],{"class":236},[172,1537,239],{"class":181},[172,1539,1540,1542,1544,1547,1550,1553,1556,1558,1561,1563,1566,1569,1572,1575,1578,1580,1583],{"class":65,"line":410},[172,1541,593],{"class":236},[172,1543,596],{"class":181},[172,1545,1546],{"class":177},"f",[172,1548,1549],{"class":229},"\"",[172,1551,1552],{"class":236},"{len",[172,1554,1555],{"class":181},"(sizes)",[172,1557,745],{"class":236},[172,1559,1560],{"class":229}," components; main holds ",[172,1562,739],{"class":236},[172,1564,1565],{"class":181},"main_size ",[172,1567,1568],{"class":177},"\u002F",[172,1570,1571],{"class":236}," len",[172,1573,1574],{"class":181},"(component_of_line)",[172,1576,1577],{"class":177},":.1%",[172,1579,745],{"class":236},[172,1581,1582],{"class":229}," of lines\"",[172,1584,608],{"class":181},[14,1586,1587,1589,1590,1594],{},[612,1588,614],{}," Treating each line as an edge between its end nodes and merging with union-find yields components in near-linear time. This uses end points only, so it reflects how a graph builder sees the network: T-junctions where a side road ends mid-line on the main road are ",[1591,1592,1593],"em",{},"not"," connected here, exactly as they would not be for a router unless the main road is split at that point. If that is how your data is modelled, split lines at their intersections first. Writing the component id back to each line and styling by it shows the isolated pieces immediately.",[137,1596,1598],{"id":1597},"fix-undershoots-and-re-check","Fix undershoots and re-check",[14,1600,1601],{},"Undershoots within the tolerance can be closed by snapping line ends to the nearest line; Processing does this for a whole layer.",[162,1603,1605],{"className":164,"code":1604,"language":166,"meta":167,"style":167},"import processing\n\nsnapped = processing.run(\"native:snapgeometries\", {\n    \"INPUT\": roads, \"REFERENCE_LAYER\": roads,\n    \"TOLERANCE\": TOL, \"BEHAVIOR\": 5,           # 5 = move end points only, prefer closest point\n    \"OUTPUT\": \"memory:roads_snapped\"})[\"OUTPUT\"]\nQgsProject.instance().addMapLayer(snapped)\n",[169,1606,1607,1614,1618,1634,1648,1673,1691],{"__ignoreMap":167},[172,1608,1609,1611],{"class":65,"line":174},[172,1610,185],{"class":177},[172,1612,1613],{"class":181}," processing\n",[172,1615,1616],{"class":65,"line":191},[172,1617,214],{"emptyLinePlaceholder":213},[172,1619,1620,1623,1625,1628,1631],{"class":65,"line":204},[172,1621,1622],{"class":181},"snapped ",[172,1624,223],{"class":177},[172,1626,1627],{"class":181}," processing.run(",[172,1629,1630],{"class":229},"\"native:snapgeometries\"",[172,1632,1633],{"class":181},", {\n",[172,1635,1636,1639,1642,1645],{"class":65,"line":210},[172,1637,1638],{"class":229},"    \"INPUT\"",[172,1640,1641],{"class":181},": roads, ",[172,1643,1644],{"class":229},"\"REFERENCE_LAYER\"",[172,1646,1647],{"class":181},": roads,\n",[172,1649,1650,1653,1656,1658,1660,1663,1665,1667,1670],{"class":65,"line":217},[172,1651,1652],{"class":229},"    \"TOLERANCE\"",[172,1654,1655],{"class":181},": ",[172,1657,706],{"class":236},[172,1659,756],{"class":181},[172,1661,1662],{"class":229},"\"BEHAVIOR\"",[172,1664,1655],{"class":181},[172,1666,80],{"class":236},[172,1668,1669],{"class":181},",           ",[172,1671,1672],{"class":254},"# 5 = move end points only, prefer closest point\n",[172,1674,1675,1678,1680,1683,1686,1689],{"class":65,"line":242},[172,1676,1677],{"class":229},"    \"OUTPUT\"",[172,1679,1655],{"class":181},[172,1681,1682],{"class":229},"\"memory:roads_snapped\"",[172,1684,1685],{"class":181},"})[",[172,1687,1688],{"class":229},"\"OUTPUT\"",[172,1690,239],{"class":181},[172,1692,1693],{"class":65,"line":258},[172,1694,1695],{"class":181},"QgsProject.instance().addMapLayer(snapped)\n",[14,1697,1698,1700,1701,1705],{},[612,1699,614],{}," Snapping a layer to itself with behaviour 5 moves only line end points, each onto the closest point of a nearby line, which closes undershoots without disturbing the interior vertices of correctly drawn lines. The line that was snapped to still has no vertex at the junction, so split lines at their intersections afterwards if a graph builder needs nodes there. Use a tolerance no larger than the one used for classification, or real dead ends near other roads get pulled across. After snapping, rerun the dangle and component checks: the undershoot count should drop to near zero and the main component should grow. ",[18,1702,1704],{"href":1703},"\u002Fspatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002Fsnap-geometries-to-layer-pyqgis\u002F","Snapping geometries to a layer"," explains each snapping behaviour.",[137,1707,1709],{"id":1708},"qgis-version-compatibility","QGIS version compatibility",[14,1711,1712,1713,1716,1717,1720,1721,1724],{},"All code runs on QGIS 3.34 LTR, 3.40 LTR and the QGIS 4 series. ",[169,1714,1715],{},"QgsSpatialIndex.nearestNeighbor"," with a maximum distance has existed since 3.8. ",[169,1718,1719],{},"native:snapgeometries"," behaviours are numbered the same across versions; check ",[169,1722,1723],{},"processing.algorithmHelp(\"native:snapgeometries\")"," for the list on your release.",[137,1726,1728],{"id":1727},"troubleshooting","Troubleshooting",[142,1730,1731,1737,1743,1749],{},[145,1732,1733,1736],{},[612,1734,1735],{},"Thousands of dangles in a clean network."," Lines meet mid-segment (T-junctions) rather than at end points; the touch test handles this, but the component count will not until lines are split.",[145,1738,1739,1742],{},[612,1740,1741],{},"Bridges are reported as missing nodes."," Filter crossings by a level or z-order attribute.",[145,1744,1745,1748],{},[612,1746,1747],{},"Snapping created odd geometry."," The tolerance was larger than the spacing between parallel lines; reduce it.",[145,1750,1751,1754],{},[612,1752,1753],{},"Coordinates do not match despite looking identical."," Increase rounding precision only if data is survey-grade; otherwise millimetres is right.",[137,1756,1758],{"id":1757},"conclusion","Conclusion",[14,1760,1761],{},"Find dangling ends by counting rounded end points and testing for touches, classify them by distance into undershoots, dead ends and stubs, check crossings for shared vertices, count connected components with union-find, snap within the tolerance and re-check until the network is one main piece.",[137,1763,1765],{"id":1764},"frequently-asked-questions","Frequently Asked Questions",[14,1767,1768,1771,1772,1775,1776,22],{},[612,1769,1770],{},"Does QGIS's network analysis library fix these errors?","\nNo. ",[169,1773,1774],{},"QgsGraphBuilder"," has a topology tolerance that merges nearby nodes, which hides small gaps but does not repair the data. See ",[18,1777,1779],{"href":1778},"\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fbuild-network-graph-with-qgsgraphbuilder\u002F","building a network graph",[14,1781,1782,1785],{},[612,1783,1784],{},"Do rivers need the same check?","\nYes, plus direction: every segment should flow towards the outlet. Dangles at sources are expected.",[14,1787,1788,1791,1792,1795,1796,1799],{},[612,1789,1790],{},"Can I run this in PostGIS?","\nYes, with pgRouting's ",[169,1793,1794],{},"pgr_analyzeGraph"," for dangles and ",[169,1797,1798],{},"pgr_connectedComponents"," for pieces.",[14,1801,1802,1805],{},[612,1803,1804],{},"What tolerance should I use for snapping?","\nThe smallest that closes your undershoots — read it off the gap distances recorded on the dangle layer.",[137,1807,1809],{"id":1808},"related","Related",[142,1811,1812,1817,1822,1827,1833],{},[145,1813,1814,1816],{},[18,1815,29],{"href":28}," — the guide this recipe belongs to",[145,1818,1819],{},[18,1820,1821],{"href":1703},"Snap Geometries to a Layer in PyQGIS",[145,1823,1824],{},[18,1825,1826],{"href":1778},"Build a Network Graph with QgsGraphBuilder",[145,1828,1829],{},[18,1830,1832],{"href":1831},"\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fsnap-points-to-network-pyqgis\u002F","Snap Points to a Network in PyQGIS",[145,1834,1835],{},[18,1836,1838],{"href":1837},"\u002Fspatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002Ffind-gaps-and-overlaps-between-polygons-pyqgis\u002F","Find Gaps and Overlaps Between Polygons in PyQGIS",[1840,1841,1842],"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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}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);}",{"title":167,"searchDepth":191,"depth":191,"links":1844},[1845,1846,1847,1848,1849,1850,1851,1852,1853,1854,1855],{"id":139,"depth":191,"text":140},{"id":156,"depth":191,"text":157},{"id":618,"depth":191,"text":619},{"id":1078,"depth":191,"text":1079},{"id":1284,"depth":191,"text":1285},{"id":1597,"depth":191,"text":1598},{"id":1708,"depth":191,"text":1709},{"id":1727,"depth":191,"text":1728},{"id":1757,"depth":191,"text":1758},{"id":1764,"depth":191,"text":1765},{"id":1808,"depth":191,"text":1809},"Find the topology errors that break routing and flow analysis in road, river and pipe networks — dangling ends, undershoots and overshoots, missing nodes at crossings and disconnected pieces — and report them as points and components you can fix.","md",{"slug":1859,"type":1860,"breadcrumb":1861,"datePublished":1862,"dateModified":1862},"check-line-network-connectivity-pyqgis","article","Check Line Network Connectivity","2026-10-02","\u002Fspatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002Fcheck-line-network-connectivity-pyqgis",{"title":5,"description":1856},"spatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002Fcheck-line-network-connectivity-pyqgis\u002Findex","gSWXfgm-uLg40Po8Wa4a8UrTCcrVgLCG2ocAyuL6pTU",1790966261401]