[{"data":1,"prerenderedAt":1780},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fjoin-attributes-by-nearest-neighbour-pyqgis":3},{"id":4,"title":5,"body":6,"description":1769,"extension":1770,"meta":1771,"navigation":247,"path":1776,"seo":1777,"stem":1778,"__hash__":1779},"docs\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fjoin-attributes-by-nearest-neighbour-pyqgis\u002Findex.md","Join Attributes by Nearest Neighbour in PyQGIS",{"type":7,"value":8,"toc":1757},"minimark",[9,13,17,26,170,175,196,200,207,514,548,554,558,561,672,922,939,943,950,1205,1227,1231,1234,1308,1574,1595,1599,1624,1628,1669,1673,1679,1683,1689,1695,1707,1713,1719,1723,1753],[10,11,5],"h1",{"id":12},"join-attributes-by-nearest-neighbour-in-pyqgis",[14,15,16],"p",{},"A spatial join by location needs the features to touch. Most real questions do not come with touching geometry: which fire station is nearest each school, which weather station should each field use, which bus stop is closest to each new housing site, what is the nearest hydrant to every building. Those are nearest-neighbour joins, and they have their own traps — a nearest neighbour always exists unless you cap the distance, ties are resolved arbitrarily, and \"nearest\" in degrees is not nearest on the ground.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002F","Geometry Operations and Spatial Predicates in PyQGIS",". It runs the Processing algorithm, asks for several neighbours with a maximum distance, reproduces the join in Python with a spatial index for full control, and covers the checks that make the results defensible.",[14,27,28],{},[29,30,35,39,43,50,59,69,76,82,87,95,101,107,112,115,118,121,124,129,134,137,140,143,147,151,159,162,166],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 300","img","Nearest neighbour join outcomes: a school joined to the nearest station within range, a school with two stations almost equally close, and a remote school whose nearest station is beyond the distance cap and gets no join","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Nearest is not always near",[40,41,42],"desc",{},"Three schools as blue squares and fire stations as red triangles. The first school has one station 1.2 km away and is joined to it with distance recorded. The second school has two stations at 2.4 and 2.5 km; the join picks one, and asking for two neighbours reveals the near tie. The third school's nearest station is 31 km away, beyond a 10 km cap, so it is left unmatched rather than joined to something meaningless.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","300","#f6f3ea",[51,52,58],"text",{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Every input finds a neighbour — unless you set a limit",[44,60],{"x":61,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"24","48","222","226","10","#e8efe6","#15803d","stroke-width:2.5",[51,70,75],{"x":71,"y":72,"style":73,"fill":74,"textAnchor":57},"135","72","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","#166534","clear match",[44,77],{"x":78,"y":79,"width":80,"height":80,"fill":81},"66","150","16","#2563eb",[83,84],"path",{"d":85,"fill":86},"M190 120 L200 138 L180 138 z","#b91c1c",[88,89],"line",{"x1":90,"y1":91,"x2":92,"y2":93,"stroke":67,"style":94},"82","156","182","132","stroke-width:2;stroke-dasharray:5 3",[51,96,100],{"x":71,"y":97,"style":98,"fill":99,"textAnchor":57},"210","text-anchor:middle;font-size:10.5px;font-family:sans-serif","#2f3b35","station 7 · 1.2 km",[51,102,106],{"x":71,"y":103,"style":104,"fill":105,"textAnchor":57},"236","text-anchor:middle;font-size:10px;font-family:sans-serif","#59645f","one obvious answer",[44,108],{"x":109,"y":62,"width":63,"height":64,"rx":65,"fill":110,"stroke":111,"style":68},"269","#fdf2e2","#b45309",[51,113,114],{"x":53,"y":72,"style":73,"fill":111,"textAnchor":57},"near tie",[44,116],{"x":117,"y":79,"width":80,"height":80,"fill":81},"372",[83,119],{"d":120,"fill":86},"M310 110 L320 128 L300 128 z",[83,122],{"d":123,"fill":86},"M460 112 L470 130 L450 130 z",[88,125],{"x1":117,"y1":126,"x2":127,"y2":128,"stroke":111,"style":94},"154","316","124",[88,130],{"x1":131,"y1":126,"x2":132,"y2":133,"stroke":111,"style":94},"388","454","126",[51,135,136],{"x":53,"y":97,"style":98,"fill":99,"textAnchor":57},"2.4 km vs 2.5 km",[51,138,139],{"x":53,"y":103,"style":104,"fill":105,"textAnchor":57},"ask for k = 2 to see it",[44,141],{"x":142,"y":62,"width":63,"height":64,"rx":65,"fill":110,"stroke":86,"style":68},"514",[51,144,146],{"x":145,"y":72,"style":73,"fill":86,"textAnchor":57},"625","beyond the cap",[44,148],{"x":149,"y":150,"width":80,"height":80,"fill":81},"560","170",[152,153],"circle",{"cx":154,"cy":155,"r":156,"fill":157,"stroke":86,"style":158},"568","178","46","none","stroke-width:1.5;stroke-dasharray:4 3",[83,160],{"d":161,"fill":86},"M704 92 L714 110 L694 110 z",[51,163,165],{"x":145,"y":164,"style":98,"fill":99,"textAnchor":57},"248","31 km > 10 km cap",[51,167,169],{"x":145,"y":168,"style":104,"fill":105,"textAnchor":57},"266","left unmatched",[171,172,174],"h2",{"id":173},"prerequisites","Prerequisites",[176,177,178,186,193],"ul",{},[179,180,181,185],"li",{},[182,183,184],"strong",{},"QGIS 3.40 LTR"," or newer, or the QGIS 4 series.",[179,187,188,189,192],{},"Two layers in the ",[182,190,191],{},"same projected CRS",". The algorithm measures in layer units, so degrees give degree distances and a nearest neighbour that is only nearest on a flat map.",[179,194,195],{},"A clear idea of the largest distance at which a match still means something for your question.",[171,197,199],{"id":198},"run-the-nearest-neighbour-join","Run the nearest-neighbour join",[14,201,202,206],{},[203,204,205],"code",{},"native:joinbynearest"," copies attributes from the nearest feature in a second layer onto each input feature and adds the distance.",[208,209,214],"pre",{"className":210,"code":211,"language":212,"meta":213,"style":213},"language-python shiki shiki-themes github-dark","import processing\nfrom qgis.core import QgsProject, QgsVectorLayer\n\nschools = QgsProject.instance().mapLayersByName(\"schools\")[0]\nstations = QgsProject.instance().mapLayersByName(\"fire_stations\")[0]\nassert schools.crs() == stations.crs() and not schools.crs().isGeographic()\n\nresult = processing.run(\"native:joinbynearest\", {\n    \"INPUT\": schools,\n    \"INPUT_2\": stations,\n    \"FIELDS_TO_COPY\": [\"station_id\", \"station_name\", \"crew_type\"],\n    \"DISCARD_NONMATCHING\": False,\n    \"PREFIX\": \"fs_\",\n    \"NEIGHBORS\": 1,\n    \"MAX_DISTANCE\": 10000,\n    \"OUTPUT\": \"\u002Fdata\u002Fwork\u002Fschools_nearest_station.gpkg\",\n})\njoined = QgsVectorLayer(result[\"OUTPUT\"], \"schools + nearest station\", \"ogr\")\nprint(result[\"JOINED_COUNT\"], \"joined,\", result[\"UNJOINABLE_COUNT\"], \"without a station in range\")\n","python","",[203,215,216,228,242,249,274,293,317,322,339,348,357,383,398,411,424,437,450,456,484],{"__ignoreMap":213},[217,218,220,224],"span",{"class":88,"line":219},1,[217,221,223],{"class":222},"snl16","import",[217,225,227],{"class":226},"s95oV"," processing\n",[217,229,231,234,237,239],{"class":88,"line":230},2,[217,232,233],{"class":222},"from",[217,235,236],{"class":226}," qgis.core ",[217,238,223],{"class":222},[217,240,241],{"class":226}," QgsProject, QgsVectorLayer\n",[217,243,245],{"class":88,"line":244},3,[217,246,248],{"emptyLinePlaceholder":247},true,"\n",[217,250,252,255,258,261,265,268,271],{"class":88,"line":251},4,[217,253,254],{"class":226},"schools ",[217,256,257],{"class":222},"=",[217,259,260],{"class":226}," QgsProject.instance().mapLayersByName(",[217,262,264],{"class":263},"sU2Wk","\"schools\"",[217,266,267],{"class":226},")[",[217,269,46],{"class":270},"sDLfK",[217,272,273],{"class":226},"]\n",[217,275,277,280,282,284,287,289,291],{"class":88,"line":276},5,[217,278,279],{"class":226},"stations ",[217,281,257],{"class":222},[217,283,260],{"class":226},[217,285,286],{"class":263},"\"fire_stations\"",[217,288,267],{"class":226},[217,290,46],{"class":270},[217,292,273],{"class":226},[217,294,296,299,302,305,308,311,314],{"class":88,"line":295},6,[217,297,298],{"class":222},"assert",[217,300,301],{"class":226}," schools.crs() ",[217,303,304],{"class":222},"==",[217,306,307],{"class":226}," stations.crs() ",[217,309,310],{"class":222},"and",[217,312,313],{"class":222}," not",[217,315,316],{"class":226}," schools.crs().isGeographic()\n",[217,318,320],{"class":88,"line":319},7,[217,321,248],{"emptyLinePlaceholder":247},[217,323,325,328,330,333,336],{"class":88,"line":324},8,[217,326,327],{"class":226},"result ",[217,329,257],{"class":222},[217,331,332],{"class":226}," processing.run(",[217,334,335],{"class":263},"\"native:joinbynearest\"",[217,337,338],{"class":226},", {\n",[217,340,342,345],{"class":88,"line":341},9,[217,343,344],{"class":263},"    \"INPUT\"",[217,346,347],{"class":226},": schools,\n",[217,349,351,354],{"class":88,"line":350},10,[217,352,353],{"class":263},"    \"INPUT_2\"",[217,355,356],{"class":226},": stations,\n",[217,358,360,363,366,369,372,375,377,380],{"class":88,"line":359},11,[217,361,362],{"class":263},"    \"FIELDS_TO_COPY\"",[217,364,365],{"class":226},": [",[217,367,368],{"class":263},"\"station_id\"",[217,370,371],{"class":226},", ",[217,373,374],{"class":263},"\"station_name\"",[217,376,371],{"class":226},[217,378,379],{"class":263},"\"crew_type\"",[217,381,382],{"class":226},"],\n",[217,384,386,389,392,395],{"class":88,"line":385},12,[217,387,388],{"class":263},"    \"DISCARD_NONMATCHING\"",[217,390,391],{"class":226},": ",[217,393,394],{"class":270},"False",[217,396,397],{"class":226},",\n",[217,399,401,404,406,409],{"class":88,"line":400},13,[217,402,403],{"class":263},"    \"PREFIX\"",[217,405,391],{"class":226},[217,407,408],{"class":263},"\"fs_\"",[217,410,397],{"class":226},[217,412,414,417,419,422],{"class":88,"line":413},14,[217,415,416],{"class":263},"    \"NEIGHBORS\"",[217,418,391],{"class":226},[217,420,421],{"class":270},"1",[217,423,397],{"class":226},[217,425,427,430,432,435],{"class":88,"line":426},15,[217,428,429],{"class":263},"    \"MAX_DISTANCE\"",[217,431,391],{"class":226},[217,433,434],{"class":270},"10000",[217,436,397],{"class":226},[217,438,440,443,445,448],{"class":88,"line":439},16,[217,441,442],{"class":263},"    \"OUTPUT\"",[217,444,391],{"class":226},[217,446,447],{"class":263},"\"\u002Fdata\u002Fwork\u002Fschools_nearest_station.gpkg\"",[217,449,397],{"class":226},[217,451,453],{"class":88,"line":452},17,[217,454,455],{"class":226},"})\n",[217,457,459,462,464,467,470,473,476,478,481],{"class":88,"line":458},18,[217,460,461],{"class":226},"joined ",[217,463,257],{"class":222},[217,465,466],{"class":226}," QgsVectorLayer(result[",[217,468,469],{"class":263},"\"OUTPUT\"",[217,471,472],{"class":226},"], ",[217,474,475],{"class":263},"\"schools + nearest station\"",[217,477,371],{"class":226},[217,479,480],{"class":263},"\"ogr\"",[217,482,483],{"class":226},")\n",[217,485,487,490,493,496,498,501,504,507,509,512],{"class":88,"line":486},19,[217,488,489],{"class":270},"print",[217,491,492],{"class":226},"(result[",[217,494,495],{"class":263},"\"JOINED_COUNT\"",[217,497,472],{"class":226},[217,499,500],{"class":263},"\"joined,\"",[217,502,503],{"class":226},", result[",[217,505,506],{"class":263},"\"UNJOINABLE_COUNT\"",[217,508,472],{"class":226},[217,510,511],{"class":263},"\"without a station in range\"",[217,513,483],{"class":226},[14,515,516,519,520,523,524,527,528,531,532,535,536,539,540,543,544,547],{},[182,517,518],{},"Breakdown:"," ",[203,521,522],{},"FIELDS_TO_COPY"," keeps the output narrow; left empty, every field of the station layer is copied. ",[203,525,526],{},"PREFIX"," avoids collisions when both layers have a ",[203,529,530],{},"name"," field. ",[203,533,534],{},"MAX_DISTANCE"," is in layer units — 10,000 metres here — and is the most important parameter: without it every school is joined to some station, however far. With ",[203,537,538],{},"DISCARD_NONMATCHING"," false, schools with no station in range stay in the output with empty join fields, which is almost always what you want for a coverage analysis, because the unmatched ones are the finding. Besides the copied fields, the algorithm adds ",[203,541,542],{},"n"," (the neighbour rank), ",[203,545,546],{},"distance",", and the coordinates of both the input and the matched feature.",[14,549,550,551,553],{},"The assertion at the top is cheap insurance. On a layer in EPSG:4326, a ",[203,552,534],{}," of 10,000 means ten thousand degrees — everything matches — and a degree of longitude shrinks towards the poles, so the nearest by degrees can be the wrong station.",[171,555,557],{"id":556},"several-neighbours-and-ties","Several neighbours, and ties",[14,559,560],{},"Asking for more than one neighbour answers different questions — the second-nearest station as a backup, the three nearest weather stations for an average — and it exposes ties the single-neighbour join hides.",[14,562,563],{},[29,564,567,570,573,576,580,587,593,596,600,602,609,613,616,619,622,625,628,631,634,637,640,643,646,649,651,654,657,660,662,665,668],{"viewBox":565,"role":32,"ariaLabel":566,"xmlns":34},"0 0 760 250","Output shape with NEIGHBORS set to 3: each input feature appears up to three times, ranked by n from 1 to 3 with increasing distance, and features with fewer neighbours in range appear fewer times",[36,568,569],{},"k neighbours means k rows per input",[40,571,572],{},"An output table for two schools with NEIGHBORS equal to 3 and a 10 km cap. School A appears three times with n equal to 1, 2 and 3 at distances 1.2, 4.8 and 7.9 km. School B appears twice, with n equal to 1 and 2 at 2.4 and 2.5 km, because its third nearest station is beyond the cap. The near-equal first and second distances for school B flag a tie worth reviewing.",[44,574],{"x":46,"y":46,"width":47,"height":575,"fill":49},"250",[51,577,579],{"x":53,"y":578,"style":55,"fill":56,"textAnchor":57},"26","NEIGHBORS = 3 multiplies rows, not columns",[44,581],{"x":61,"y":582,"width":583,"height":584,"rx":65,"fill":585,"stroke":105,"style":586},"44","712","186","#fffdf7","stroke-width:2",[51,588,592],{"x":589,"y":590,"style":591,"fill":56,"textAnchor":57},"120","70","text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:sans-serif","school",[51,594,542],{"x":595,"y":590,"style":591,"fill":56,"textAnchor":57},"260",[51,597,599],{"x":598,"y":590,"style":591,"fill":56,"textAnchor":57},"400","fs_station_id",[51,601,546],{"x":149,"y":590,"style":591,"fill":56,"textAnchor":57},[88,603],{"x1":604,"y1":605,"x2":606,"y2":605,"stroke":607,"style":608},"40","80","720","#d9d3c4","stroke-width:1.5",[51,610,612],{"x":589,"y":611,"style":104,"fill":99,"textAnchor":57},"104","A",[51,614,421],{"x":595,"y":611,"style":615,"fill":99,"textAnchor":57},"text-anchor:middle;font-size:10px;font-family:monospace",[51,617,618],{"x":598,"y":611,"style":615,"fill":99,"textAnchor":57},"FS07",[51,620,621],{"x":149,"y":611,"style":615,"fill":99,"textAnchor":57},"1,210",[51,623,612],{"x":589,"y":624,"style":104,"fill":99,"textAnchor":57},"128",[51,626,627],{"x":595,"y":624,"style":615,"fill":99,"textAnchor":57},"2",[51,629,630],{"x":598,"y":624,"style":615,"fill":99,"textAnchor":57},"FS03",[51,632,633],{"x":149,"y":624,"style":615,"fill":99,"textAnchor":57},"4,780",[51,635,612],{"x":589,"y":636,"style":104,"fill":99,"textAnchor":57},"152",[51,638,639],{"x":595,"y":636,"style":615,"fill":99,"textAnchor":57},"3",[51,641,642],{"x":598,"y":636,"style":615,"fill":99,"textAnchor":57},"FS11",[51,644,645],{"x":149,"y":636,"style":615,"fill":99,"textAnchor":57},"7,905",[51,647,648],{"x":589,"y":92,"style":104,"fill":111,"textAnchor":57},"B",[51,650,421],{"x":595,"y":92,"style":615,"fill":111,"textAnchor":57},[51,652,653],{"x":598,"y":92,"style":615,"fill":111,"textAnchor":57},"FS02",[51,655,656],{"x":149,"y":92,"style":615,"fill":111,"textAnchor":57},"2,412",[51,658,648],{"x":589,"y":659,"style":104,"fill":111,"textAnchor":57},"206",[51,661,627],{"x":595,"y":659,"style":615,"fill":111,"textAnchor":57},[51,663,664],{"x":598,"y":659,"style":615,"fill":111,"textAnchor":57},"FS09",[51,666,667],{"x":149,"y":659,"style":615,"fill":111,"textAnchor":57},"2,498",[51,669,114],{"x":670,"y":671,"style":104,"fill":111,"textAnchor":57},"660","194",[208,673,675],{"className":210,"code":674,"language":212,"meta":213,"style":213},"from qgis.core import QgsVectorLayer, QgsFeatureRequest\n\nthree = processing.run(\"native:joinbynearest\", {\n    \"INPUT\": schools, \"INPUT_2\": stations,\n    \"FIELDS_TO_COPY\": [\"station_id\"], \"PREFIX\": \"fs_\",\n    \"NEIGHBORS\": 3, \"MAX_DISTANCE\": 10000, \"DISCARD_NONMATCHING\": False,\n    \"OUTPUT\": \"TEMPORARY_OUTPUT\",\n})[\"OUTPUT\"]\n\nby_school = {}\nfor f in three.getFeatures(QgsFeatureRequest().addOrderBy(\"distance\")):\n    by_school.setdefault(f[\"school_id\"], []).append(f[\"distance\"])\n\nties = {sid: d for sid, d in by_school.items()\n        if len(d) > 1 and d[1] - d[0] \u003C 150}\nprint(len(ties), \"schools whose two nearest stations are within 150 m of each other\")\n",[203,676,677,688,692,705,717,736,764,775,784,788,798,818,834,838,858,904],{"__ignoreMap":213},[217,678,679,681,683,685],{"class":88,"line":219},[217,680,233],{"class":222},[217,682,236],{"class":226},[217,684,223],{"class":222},[217,686,687],{"class":226}," QgsVectorLayer, QgsFeatureRequest\n",[217,689,690],{"class":88,"line":230},[217,691,248],{"emptyLinePlaceholder":247},[217,693,694,697,699,701,703],{"class":88,"line":244},[217,695,696],{"class":226},"three ",[217,698,257],{"class":222},[217,700,332],{"class":226},[217,702,335],{"class":263},[217,704,338],{"class":226},[217,706,707,709,712,715],{"class":88,"line":251},[217,708,344],{"class":263},[217,710,711],{"class":226},": schools, ",[217,713,714],{"class":263},"\"INPUT_2\"",[217,716,356],{"class":226},[217,718,719,721,723,725,727,730,732,734],{"class":88,"line":276},[217,720,362],{"class":263},[217,722,365],{"class":226},[217,724,368],{"class":263},[217,726,472],{"class":226},[217,728,729],{"class":263},"\"PREFIX\"",[217,731,391],{"class":226},[217,733,408],{"class":263},[217,735,397],{"class":226},[217,737,738,740,742,744,746,749,751,753,755,758,760,762],{"class":88,"line":295},[217,739,416],{"class":263},[217,741,391],{"class":226},[217,743,639],{"class":270},[217,745,371],{"class":226},[217,747,748],{"class":263},"\"MAX_DISTANCE\"",[217,750,391],{"class":226},[217,752,434],{"class":270},[217,754,371],{"class":226},[217,756,757],{"class":263},"\"DISCARD_NONMATCHING\"",[217,759,391],{"class":226},[217,761,394],{"class":270},[217,763,397],{"class":226},[217,765,766,768,770,773],{"class":88,"line":319},[217,767,442],{"class":263},[217,769,391],{"class":226},[217,771,772],{"class":263},"\"TEMPORARY_OUTPUT\"",[217,774,397],{"class":226},[217,776,777,780,782],{"class":88,"line":324},[217,778,779],{"class":226},"})[",[217,781,469],{"class":263},[217,783,273],{"class":226},[217,785,786],{"class":88,"line":341},[217,787,248],{"emptyLinePlaceholder":247},[217,789,790,793,795],{"class":88,"line":350},[217,791,792],{"class":226},"by_school ",[217,794,257],{"class":222},[217,796,797],{"class":226}," {}\n",[217,799,800,803,806,809,812,815],{"class":88,"line":359},[217,801,802],{"class":222},"for",[217,804,805],{"class":226}," f ",[217,807,808],{"class":222},"in",[217,810,811],{"class":226}," three.getFeatures(QgsFeatureRequest().addOrderBy(",[217,813,814],{"class":263},"\"distance\"",[217,816,817],{"class":226},")):\n",[217,819,820,823,826,829,831],{"class":88,"line":385},[217,821,822],{"class":226},"    by_school.setdefault(f[",[217,824,825],{"class":263},"\"school_id\"",[217,827,828],{"class":226},"], []).append(f[",[217,830,814],{"class":263},[217,832,833],{"class":226},"])\n",[217,835,836],{"class":88,"line":400},[217,837,248],{"emptyLinePlaceholder":247},[217,839,840,843,845,848,850,853,855],{"class":88,"line":413},[217,841,842],{"class":226},"ties ",[217,844,257],{"class":222},[217,846,847],{"class":226}," {sid: d ",[217,849,802],{"class":222},[217,851,852],{"class":226}," sid, d ",[217,854,808],{"class":222},[217,856,857],{"class":226}," by_school.items()\n",[217,859,860,863,866,869,872,875,878,881,883,886,889,891,893,895,898,901],{"class":88,"line":426},[217,861,862],{"class":222},"        if",[217,864,865],{"class":270}," len",[217,867,868],{"class":226},"(d) ",[217,870,871],{"class":222},">",[217,873,874],{"class":270}," 1",[217,876,877],{"class":222}," and",[217,879,880],{"class":226}," d[",[217,882,421],{"class":270},[217,884,885],{"class":226},"] ",[217,887,888],{"class":222},"-",[217,890,880],{"class":226},[217,892,46],{"class":270},[217,894,885],{"class":226},[217,896,897],{"class":222},"\u003C",[217,899,900],{"class":270}," 150",[217,902,903],{"class":226},"}\n",[217,905,906,908,911,914,917,920],{"class":88,"line":439},[217,907,489],{"class":270},[217,909,910],{"class":226},"(",[217,912,913],{"class":270},"len",[217,915,916],{"class":226},"(ties), ",[217,918,919],{"class":263},"\"schools whose two nearest stations are within 150 m of each other\"",[217,921,483],{"class":226},[14,923,924,926,927,930,931,933,934,938],{},[182,925,518],{}," With ",[203,928,929],{},"NEIGHBORS"," above one, each input feature appears once per neighbour found, ranked by ",[203,932,542],{},", so the output has more rows than the input — plan any later aggregation accordingly. Ordering by distance and grouping by the school's own id gives a sorted list of distances per school. A gap under 150 m between first and second is well within the uncertainty of where a building's centroid is, so for those schools \"the nearest station\" is a coin toss; flagging them is more honest than reporting whichever one the algorithm picked. For response-time questions, straight-line distance is only a first cut anyway — ",[21,935,937],{"href":936},"\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fcalculate-service-areas-pyqgis\u002F","network service areas"," answer the real one.",[171,940,942],{"id":941},"the-same-join-in-python-with-a-spatial-index","The same join in Python with a spatial index",[14,944,945,946,949],{},"When the join is part of a larger script, or when you need behaviour the algorithm does not offer — skipping candidates with a certain attribute, distance to polygon edges rather than centroids, custom tie-breaking — use ",[203,947,948],{},"QgsSpatialIndex.nearestNeighbor"," directly.",[208,951,953],{"className":210,"code":952,"language":212,"meta":213,"style":213},"from qgis.core import QgsSpatialIndex, QgsFeatureRequest\n\nindex = QgsSpatialIndex(stations.getFeatures(\n    QgsFeatureRequest().setFilterExpression(\"\\\"crew_type\\\" = 'full-time'\")),\n    flags=QgsSpatialIndex.FlagStoreFeatureGeometries)\n\nnearest = {}\nfor school in schools.getFeatures():\n    geom = school.geometry()\n    candidate_ids = index.nearestNeighbor(geom, 1, 10000)\n    if not candidate_ids:\n        nearest[school[\"school_id\"]] = (None, None)\n        continue\n    best = min(candidate_ids, key=lambda fid: index.geometry(fid).distance(geom))\n    nearest[school[\"school_id\"]] = (best, round(index.geometry(best).distance(geom)))\n\nunmatched = [k for k, v in nearest.items() if v[0] is None]\nprint(len(unmatched), \"schools with no full-time station within 10 km\")\n",[203,954,955,966,970,980,1002,1013,1017,1026,1038,1048,1066,1076,1100,1105,1127,1147,1151,1189],{"__ignoreMap":213},[217,956,957,959,961,963],{"class":88,"line":219},[217,958,233],{"class":222},[217,960,236],{"class":226},[217,962,223],{"class":222},[217,964,965],{"class":226}," QgsSpatialIndex, QgsFeatureRequest\n",[217,967,968],{"class":88,"line":230},[217,969,248],{"emptyLinePlaceholder":247},[217,971,972,975,977],{"class":88,"line":244},[217,973,974],{"class":226},"index ",[217,976,257],{"class":222},[217,978,979],{"class":226}," QgsSpatialIndex(stations.getFeatures(\n",[217,981,982,985,988,991,994,996,999],{"class":88,"line":251},[217,983,984],{"class":226},"    QgsFeatureRequest().setFilterExpression(",[217,986,987],{"class":263},"\"",[217,989,990],{"class":270},"\\\"",[217,992,993],{"class":263},"crew_type",[217,995,990],{"class":270},[217,997,998],{"class":263}," = 'full-time'\"",[217,1000,1001],{"class":226},")),\n",[217,1003,1004,1008,1010],{"class":88,"line":276},[217,1005,1007],{"class":1006},"s9osk","    flags",[217,1009,257],{"class":222},[217,1011,1012],{"class":226},"QgsSpatialIndex.FlagStoreFeatureGeometries)\n",[217,1014,1015],{"class":88,"line":295},[217,1016,248],{"emptyLinePlaceholder":247},[217,1018,1019,1022,1024],{"class":88,"line":319},[217,1020,1021],{"class":226},"nearest ",[217,1023,257],{"class":222},[217,1025,797],{"class":226},[217,1027,1028,1030,1033,1035],{"class":88,"line":324},[217,1029,802],{"class":222},[217,1031,1032],{"class":226}," school ",[217,1034,808],{"class":222},[217,1036,1037],{"class":226}," schools.getFeatures():\n",[217,1039,1040,1043,1045],{"class":88,"line":341},[217,1041,1042],{"class":226},"    geom ",[217,1044,257],{"class":222},[217,1046,1047],{"class":226}," school.geometry()\n",[217,1049,1050,1053,1055,1058,1060,1062,1064],{"class":88,"line":350},[217,1051,1052],{"class":226},"    candidate_ids ",[217,1054,257],{"class":222},[217,1056,1057],{"class":226}," index.nearestNeighbor(geom, ",[217,1059,421],{"class":270},[217,1061,371],{"class":226},[217,1063,434],{"class":270},[217,1065,483],{"class":226},[217,1067,1068,1071,1073],{"class":88,"line":359},[217,1069,1070],{"class":222},"    if",[217,1072,313],{"class":222},[217,1074,1075],{"class":226}," candidate_ids:\n",[217,1077,1078,1081,1083,1086,1088,1091,1094,1096,1098],{"class":88,"line":385},[217,1079,1080],{"class":226},"        nearest[school[",[217,1082,825],{"class":263},[217,1084,1085],{"class":226},"]] ",[217,1087,257],{"class":222},[217,1089,1090],{"class":226}," (",[217,1092,1093],{"class":270},"None",[217,1095,371],{"class":226},[217,1097,1093],{"class":270},[217,1099,483],{"class":226},[217,1101,1102],{"class":88,"line":400},[217,1103,1104],{"class":222},"        continue\n",[217,1106,1107,1110,1112,1115,1118,1121,1124],{"class":88,"line":413},[217,1108,1109],{"class":226},"    best ",[217,1111,257],{"class":222},[217,1113,1114],{"class":270}," min",[217,1116,1117],{"class":226},"(candidate_ids, ",[217,1119,1120],{"class":1006},"key",[217,1122,1123],{"class":222},"=lambda",[217,1125,1126],{"class":226}," fid: index.geometry(fid).distance(geom))\n",[217,1128,1129,1132,1134,1136,1138,1141,1144],{"class":88,"line":426},[217,1130,1131],{"class":226},"    nearest[school[",[217,1133,825],{"class":263},[217,1135,1085],{"class":226},[217,1137,257],{"class":222},[217,1139,1140],{"class":226}," (best, ",[217,1142,1143],{"class":270},"round",[217,1145,1146],{"class":226},"(index.geometry(best).distance(geom)))\n",[217,1148,1149],{"class":88,"line":439},[217,1150,248],{"emptyLinePlaceholder":247},[217,1152,1153,1156,1158,1161,1163,1166,1168,1171,1174,1177,1179,1181,1184,1187],{"class":88,"line":452},[217,1154,1155],{"class":226},"unmatched ",[217,1157,257],{"class":222},[217,1159,1160],{"class":226}," [k ",[217,1162,802],{"class":222},[217,1164,1165],{"class":226}," k, v ",[217,1167,808],{"class":222},[217,1169,1170],{"class":226}," nearest.items() ",[217,1172,1173],{"class":222},"if",[217,1175,1176],{"class":226}," v[",[217,1178,46],{"class":270},[217,1180,885],{"class":226},[217,1182,1183],{"class":222},"is",[217,1185,1186],{"class":270}," None",[217,1188,273],{"class":226},[217,1190,1191,1193,1195,1197,1200,1203],{"class":88,"line":458},[217,1192,489],{"class":270},[217,1194,910],{"class":226},[217,1196,913],{"class":270},[217,1198,1199],{"class":226},"(unmatched), ",[217,1201,1202],{"class":263},"\"schools with no full-time station within 10 km\"",[217,1204,483],{"class":226},[14,1206,1207,1209,1210,1213,1214,1217,1218,1221,1222,1226],{},[182,1208,518],{}," Building the index from a filtered request restricts candidates to full-time stations without creating an intermediate layer. ",[203,1211,1212],{},"FlagStoreFeatureGeometries"," keeps geometries inside the index so exact distances can be computed without fetching features again. ",[203,1215,1216],{},"nearestNeighbor(geometry, k, maxDistance)"," may return more than ",[203,1219,1220],{},"k"," ids when several are equally near, which is why the code takes the minimum by exact distance rather than trusting the first id. Passing a geometry rather than a point means polygon inputs are measured edge to edge, not centroid to centroid — the right behaviour for \"nearest hydrant to a building\". The general pattern is covered in ",[21,1223,1225],{"href":1224},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fbuild-spatial-index-pyqgis\u002F","building and using a spatial index",".",[171,1228,1230],{"id":1229},"write-the-result-back-and-summarise-coverage","Write the result back and summarise coverage",[14,1232,1233],{},"The dictionary is only useful once it is on the map. Adding two fields and updating them in one edit session makes the result part of the school layer itself.",[14,1235,1236],{},[29,1237,1240,1243,1246,1248,1251,1255,1258,1262,1266,1272,1276,1281,1285,1289,1292,1295,1298,1301,1305],{"viewBox":1238,"role":32,"ariaLabel":1239,"xmlns":34},"0 0 760 236","A coverage summary from the nearest-station distances: most schools within 3 km, a smaller share between 3 and 10 km, and a handful beyond 10 km with no match, shown as a stacked bar",[36,1241,1242],{},"Distances become a coverage answer",[40,1244,1245],{},"A stacked horizontal bar of 412 schools. 318 are within 3 km of a full-time station, 81 are between 3 and 10 km, and 13 have no full-time station within 10 km. The 13 are the policy finding, and they only appear because the distance cap was set and non-matching features were kept.",[44,1247],{"x":46,"y":46,"width":47,"height":103,"fill":49},[51,1249,1250],{"x":53,"y":578,"style":55,"fill":56,"textAnchor":57},"412 schools by distance to a full-time station",[44,1252],{"x":604,"y":590,"width":1253,"height":582,"rx":1254,"fill":607},"680","4",[44,1256],{"x":604,"y":590,"width":1257,"height":582,"rx":1254,"fill":67},"525",[44,1259],{"x":1260,"y":590,"width":1261,"height":582,"fill":111},"565","134",[44,1263],{"x":1264,"y":590,"width":1265,"height":582,"fill":86},"699","21",[51,1267,1271],{"x":1268,"y":1269,"style":1270,"fill":585,"textAnchor":57},"302","97","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","318 within 3 km",[51,1273,1275],{"x":1274,"y":1269,"style":1270,"fill":585,"textAnchor":57},"632","81",[44,1277],{"x":604,"y":1278,"width":97,"height":72,"rx":1279,"fill":66,"stroke":67,"style":1280},"140","8","stroke-width:1.8",[51,1282,1284],{"x":1283,"y":150,"style":98,"fill":99,"textAnchor":57},"145","under 3 km",[51,1286,1288],{"x":1283,"y":1287,"style":104,"fill":105,"textAnchor":57},"192","77%",[44,1290],{"x":1291,"y":1278,"width":97,"height":72,"rx":1279,"fill":110,"stroke":111,"style":1280},"275",[51,1293,1294],{"x":53,"y":150,"style":98,"fill":99,"textAnchor":57},"3 – 10 km",[51,1296,1297],{"x":53,"y":1287,"style":104,"fill":105,"textAnchor":57},"20%",[44,1299],{"x":1300,"y":1278,"width":97,"height":72,"rx":1279,"fill":110,"stroke":86,"style":1280},"510",[51,1302,1304],{"x":1303,"y":150,"style":591,"fill":86,"textAnchor":57},"615","none within 10 km: 13",[51,1306,1307],{"x":1303,"y":1287,"style":104,"fill":105,"textAnchor":57},"the finding",[208,1309,1311],{"className":210,"code":1310,"language":212,"meta":213,"style":213},"from qgis.core import QgsField, edit\nfrom qgis.PyQt.QtCore import QMetaType\n\nwith edit(schools):\n    for name, kind in ((\"nearest_fs\", QMetaType.Type.LongLong), (\"fs_dist_m\", QMetaType.Type.Int)):\n        if schools.fields().indexOf(name) \u003C 0:\n            schools.addAttribute(QgsField(name, kind))\n    schools.updateFields()\n    i_fs = schools.fields().indexOf(\"nearest_fs\")\n    i_d = schools.fields().indexOf(\"fs_dist_m\")\n    for school in schools.getFeatures():\n        fid, dist = nearest[school[\"school_id\"]]\n        schools.changeAttributeValues(school.id(), {i_fs: fid, i_d: dist})\n\nbands = {\"under 3 km\": 0, \"3–10 km\": 0, \"none within 10 km\": 0}\nfor _, dist in nearest.values():\n    key = (\"none within 10 km\" if dist is None\n           else \"under 3 km\" if dist \u003C 3000 else \"3–10 km\")\n    bands[key] += 1\nprint(bands)\n",[203,1312,1313,1324,1336,1340,1348,1373,1388,1393,1398,1412,1425,1435,1450,1455,1459,1496,1508,1530,1555,1566],{"__ignoreMap":213},[217,1314,1315,1317,1319,1321],{"class":88,"line":219},[217,1316,233],{"class":222},[217,1318,236],{"class":226},[217,1320,223],{"class":222},[217,1322,1323],{"class":226}," QgsField, edit\n",[217,1325,1326,1328,1331,1333],{"class":88,"line":230},[217,1327,233],{"class":222},[217,1329,1330],{"class":226}," qgis.PyQt.QtCore ",[217,1332,223],{"class":222},[217,1334,1335],{"class":226}," QMetaType\n",[217,1337,1338],{"class":88,"line":244},[217,1339,248],{"emptyLinePlaceholder":247},[217,1341,1342,1345],{"class":88,"line":251},[217,1343,1344],{"class":222},"with",[217,1346,1347],{"class":226}," edit(schools):\n",[217,1349,1350,1353,1356,1358,1361,1364,1367,1370],{"class":88,"line":276},[217,1351,1352],{"class":222},"    for",[217,1354,1355],{"class":226}," name, kind ",[217,1357,808],{"class":222},[217,1359,1360],{"class":226}," ((",[217,1362,1363],{"class":263},"\"nearest_fs\"",[217,1365,1366],{"class":226},", QMetaType.Type.LongLong), (",[217,1368,1369],{"class":263},"\"fs_dist_m\"",[217,1371,1372],{"class":226},", QMetaType.Type.Int)):\n",[217,1374,1375,1377,1380,1382,1385],{"class":88,"line":295},[217,1376,862],{"class":222},[217,1378,1379],{"class":226}," schools.fields().indexOf(name) ",[217,1381,897],{"class":222},[217,1383,1384],{"class":270}," 0",[217,1386,1387],{"class":226},":\n",[217,1389,1390],{"class":88,"line":319},[217,1391,1392],{"class":226},"            schools.addAttribute(QgsField(name, kind))\n",[217,1394,1395],{"class":88,"line":324},[217,1396,1397],{"class":226},"    schools.updateFields()\n",[217,1399,1400,1403,1405,1408,1410],{"class":88,"line":341},[217,1401,1402],{"class":226},"    i_fs ",[217,1404,257],{"class":222},[217,1406,1407],{"class":226}," schools.fields().indexOf(",[217,1409,1363],{"class":263},[217,1411,483],{"class":226},[217,1413,1414,1417,1419,1421,1423],{"class":88,"line":350},[217,1415,1416],{"class":226},"    i_d ",[217,1418,257],{"class":222},[217,1420,1407],{"class":226},[217,1422,1369],{"class":263},[217,1424,483],{"class":226},[217,1426,1427,1429,1431,1433],{"class":88,"line":359},[217,1428,1352],{"class":222},[217,1430,1032],{"class":226},[217,1432,808],{"class":222},[217,1434,1037],{"class":226},[217,1436,1437,1440,1442,1445,1447],{"class":88,"line":385},[217,1438,1439],{"class":226},"        fid, dist ",[217,1441,257],{"class":222},[217,1443,1444],{"class":226}," nearest[school[",[217,1446,825],{"class":263},[217,1448,1449],{"class":226},"]]\n",[217,1451,1452],{"class":88,"line":400},[217,1453,1454],{"class":226},"        schools.changeAttributeValues(school.id(), {i_fs: fid, i_d: dist})\n",[217,1456,1457],{"class":88,"line":413},[217,1458,248],{"emptyLinePlaceholder":247},[217,1460,1461,1464,1466,1469,1472,1474,1476,1478,1481,1483,1485,1487,1490,1492,1494],{"class":88,"line":426},[217,1462,1463],{"class":226},"bands ",[217,1465,257],{"class":222},[217,1467,1468],{"class":226}," {",[217,1470,1471],{"class":263},"\"under 3 km\"",[217,1473,391],{"class":226},[217,1475,46],{"class":270},[217,1477,371],{"class":226},[217,1479,1480],{"class":263},"\"3–10 km\"",[217,1482,391],{"class":226},[217,1484,46],{"class":270},[217,1486,371],{"class":226},[217,1488,1489],{"class":263},"\"none within 10 km\"",[217,1491,391],{"class":226},[217,1493,46],{"class":270},[217,1495,903],{"class":226},[217,1497,1498,1500,1503,1505],{"class":88,"line":439},[217,1499,802],{"class":222},[217,1501,1502],{"class":226}," _, dist ",[217,1504,808],{"class":222},[217,1506,1507],{"class":226}," nearest.values():\n",[217,1509,1510,1513,1515,1517,1519,1522,1525,1527],{"class":88,"line":452},[217,1511,1512],{"class":226},"    key ",[217,1514,257],{"class":222},[217,1516,1090],{"class":226},[217,1518,1489],{"class":263},[217,1520,1521],{"class":222}," if",[217,1523,1524],{"class":226}," dist ",[217,1526,1183],{"class":222},[217,1528,1529],{"class":270}," None\n",[217,1531,1532,1535,1538,1540,1542,1544,1547,1550,1553],{"class":88,"line":458},[217,1533,1534],{"class":222},"           else",[217,1536,1537],{"class":263}," \"under 3 km\"",[217,1539,1521],{"class":222},[217,1541,1524],{"class":226},[217,1543,897],{"class":222},[217,1545,1546],{"class":270}," 3000",[217,1548,1549],{"class":222}," else",[217,1551,1552],{"class":263}," \"3–10 km\"",[217,1554,483],{"class":226},[217,1556,1557,1560,1563],{"class":88,"line":486},[217,1558,1559],{"class":226},"    bands[key] ",[217,1561,1562],{"class":222},"+=",[217,1564,1565],{"class":270}," 1\n",[217,1567,1569,1571],{"class":88,"line":1568},20,[217,1570,489],{"class":270},[217,1572,1573],{"class":226},"(bands)\n",[14,1575,1576,1578,1579,1582,1583,1587,1588,1591,1592,1594],{},[182,1577,518],{}," Adding fields and changing values inside one ",[203,1580,1581],{},"edit"," block commits them together or not at all, as described in ",[21,1584,1586],{"href":1585},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fedit-features-with-transactions-pyqgis\u002F","editing features with transactions",". ",[203,1589,1590],{},"changeAttributeValues"," takes a dictionary of field index to value, so both fields update in one call per feature. Writing ",[203,1593,1093],{}," leaves the fields null for unmatched schools, which keeps them distinguishable from a real distance of zero. The banded counts are the one-line answer a report needs, and they only exist because the distance cap was set and unmatched features were kept.",[171,1596,1598],{"id":1597},"qgis-version-compatibility","QGIS version compatibility",[14,1600,1601,1603,1604,1606,1607,1609,1610,1612,1613,1616,1617,1606,1620,1623],{},[203,1602,205],{}," has been available since QGIS 3.8, with ",[203,1605,929],{}," and ",[203,1608,534],{}," from the start. ",[203,1611,948],{}," accepts a geometry and a maximum distance from 3.8 as well; earlier releases take only a point and a count. ",[203,1614,1615],{},"QMetaType.Type"," field types are required on the QGIS 4 series; use ",[203,1618,1619],{},"QVariant.LongLong",[203,1621,1622],{},"QVariant.Int"," on releases before 3.38.",[171,1625,1627],{"id":1626},"troubleshooting","Troubleshooting",[176,1629,1630,1639,1645,1657,1663],{},[179,1631,1632,1635,1636,1638],{},[182,1633,1634],{},"Every feature matches, including remote ones."," No ",[203,1637,534],{},", or the layers are in degrees.",[179,1640,1641,1644],{},[182,1642,1643],{},"Distances are tiny decimals."," Geographic CRS; reproject both layers to a projected CRS.",[179,1646,1647,519,1650,1652,1653,1656],{},[182,1648,1649],{},"The output has more rows than the input.",[203,1651,929],{}," is above one; filter to ",[203,1654,1655],{},"n = 1"," for one row per input.",[179,1658,1659,1662],{},[182,1660,1661],{},"Results differ between runs for a few features."," Exact ties are resolved in index order; break ties explicitly by an attribute if it matters.",[179,1664,1665,1668],{},[182,1666,1667],{},"Polygons measured from centroids."," Use the Python route with geometries in the index for edge-to-edge distance.",[171,1670,1672],{"id":1671},"conclusion","Conclusion",[14,1674,1675,1676,1678],{},"Put both layers in a projected CRS, always set a maximum distance, and keep unmatched features — they are usually the answer. Ask for two or three neighbours to reveal near ties, drop to ",[203,1677,948],{}," when you need filtering or custom tie-breaking, and write the nearest id and distance back to the layer so the result can be mapped and summarised.",[171,1680,1682],{"id":1681},"frequently-asked-questions","Frequently Asked Questions",[14,1684,1685,1688],{},[182,1686,1687],{},"Is nearest-neighbour distance the same as travel distance?","\nNo. It is straight-line distance. Use network analysis when access follows roads.",[14,1690,1691,1694],{},[182,1692,1693],{},"Can I join lines or polygons, not just points?","\nYes. Distances are measured between geometries, so a polygon input measures to its nearest edge.",[14,1696,1697,1700,1701,1703,1704,1706],{},[182,1698,1699],{},"How do I find the nearest feature within the same layer?","\nUse the same layer for both inputs with ",[203,1702,929],{}," set to 2 and discard ",[203,1705,1655],{},", which is each feature matching itself.",[14,1708,1709,1712],{},[182,1710,1711],{},"Why is the join slow on large layers?","\nIt is not usually — both routes use spatial indexes. Check that the second layer is not a remote service being fetched repeatedly; copy it locally first.",[14,1714,1715,1718],{},[182,1716,1717],{},"What distance cap should I choose?","\nChoose it from the question, not the data. For emergency cover it is the distance that still meets a response standard; for assigning weather stations it is the distance over which conditions are plausibly similar. Record the cap in the output layer's metadata so readers know what \"no match\" means.",[171,1720,1722],{"id":1721},"related","Related",[176,1724,1725,1730,1735,1741,1747],{},[179,1726,1727,1729],{},[21,1728,24],{"href":23}," — the guide this recipe belongs to",[179,1731,1732],{},[21,1733,1734],{"href":1224},"Build and Use a Spatial Index in PyQGIS",[179,1736,1737],{},[21,1738,1740],{"href":1739},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fcalculate-distance-between-features-pyqgis\u002F","Calculate the Distance Between Features in PyQGIS",[179,1742,1743],{},[21,1744,1746],{"href":1745},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fspatial-join-points-in-polygons-pyqgis\u002F","Spatial Join Points to Polygons in PyQGIS",[179,1748,1749],{},[21,1750,1752],{"href":1751},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Flinear-referencing-along-lines-pyqgis\u002F","Linear Referencing Along Lines in PyQGIS",[1754,1755,1756],"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 .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 .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":213,"searchDepth":230,"depth":230,"links":1758},[1759,1760,1761,1762,1763,1764,1765,1766,1767,1768],{"id":173,"depth":230,"text":174},{"id":198,"depth":230,"text":199},{"id":556,"depth":230,"text":557},{"id":941,"depth":230,"text":942},{"id":1229,"depth":230,"text":1230},{"id":1597,"depth":230,"text":1598},{"id":1626,"depth":230,"text":1627},{"id":1671,"depth":230,"text":1672},{"id":1681,"depth":230,"text":1682},{"id":1721,"depth":230,"text":1722},"Attach the attributes of the closest feature in another layer with native:joinbynearest, find the k nearest neighbours with a distance cap, do the same in Python with QgsSpatialIndex, and handle ties, geographic CRSs and features with no neighbour in range.","md",{"slug":1772,"type":1773,"breadcrumb":1774,"datePublished":1775,"dateModified":1775},"join-attributes-by-nearest-neighbour-pyqgis","article","Join by Nearest Neighbour","2026-09-17","\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fjoin-attributes-by-nearest-neighbour-pyqgis",{"title":5,"description":1769},"spatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fjoin-attributes-by-nearest-neighbour-pyqgis\u002Findex","hzzISdxUDlqTfHdDlDJJfw7hZjxZcn9B6VQf9t2qTGo",1789632908414]