[{"data":1,"prerenderedAt":2150},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Ffind-nearest-facility-pyqgis":3},{"id":4,"title":5,"body":6,"description":2139,"extension":2140,"meta":2141,"navigation":224,"path":2146,"seo":2147,"stem":2148,"__hash__":2149},"docs\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Ffind-nearest-facility-pyqgis\u002Findex.md","Find the Nearest Facility by Network Distance in PyQGIS",{"type":7,"value":8,"toc":2125},"minimark",[9,13,17,26,135,140,165,169,172,562,578,582,590,678,885,893,897,900,1112,1126,1130,1133,1321,1326,1330,1333,1608,1613,1617,1620,1676,1983,1993,1997,2021,2025,2051,2055,2058,2062,2068,2074,2083,2089,2093,2121],[10,11,5],"h1",{"id":12},"find-the-nearest-facility-by-network-distance-in-pyqgis",[14,15,16],"p",{},"\"Which fire station is closest to this address?\" sounds like a nearest-neighbour question, and in straight-line terms it is. On a road network the answer is often different: a river with one bridge, a motorway without a junction, a one-way system — any of them can make the geometrically nearest station the slower one to arrive from. Network nearest-facility analysis assigns each location to the facility with the lowest travel cost along the roads, which is what planning, emergency response and service allocation actually need.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002F","Network Analysis & Routing",". It builds a graph once, runs one shortest-path tree per facility rather than one route per location, keeps the best facility for every location, compares results with straight-line assignment, and handles unreachable locations and facility capacity.",[14,27,28],{},[29,30,35,39,43,50,59,68,73,80,87,93,98,103,107,112,117,123,129],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 280","img","An address nearer to station A in a straight line but nearer to station B by road because of a river with a distant bridge","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Straight-line nearest versus network nearest",[40,41,42],"desc",{},"An address beside a river. Station A is 600 metres away in a straight line, but across the river with the nearest bridge two kilometres upstream, so its road distance is 4.1 kilometres. Station B is 1.4 kilometres away in a straight line and 1.8 kilometres by road on the same bank. Network analysis assigns the address to B.",[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","Nearest by crow, or by road?",[44,60],{"x":61,"y":62,"width":63,"height":64,"rx":46,"fill":65,"stroke":66,"style":67},"40","150","680","22","#eff3ff","#2563eb","stroke-width:1",[51,69,72],{"x":53,"y":70,"style":71,"fill":66,"textAnchor":57},"190","text-anchor:middle;font-size:10.5px;font-family:sans-serif","river",[44,74],{"x":75,"y":76,"width":61,"height":77,"rx":46,"fill":78,"stroke":79,"style":67},"600","140","42","#d9d3c4","#59645f",[51,81,86],{"x":82,"y":83,"style":84,"fill":79,"textAnchor":85},"648","136","text-anchor:start;font-size:10.0px;font-family:sans-serif","start","bridge",[88,89],"circle",{"cx":90,"cy":91,"r":92,"fill":56,"stroke":56,"style":67},"300","220","8",[51,94,97],{"x":90,"y":95,"style":71,"fill":96,"textAnchor":57},"250","#2f3b35","address",[88,99],{"cx":90,"cy":100,"r":101,"fill":102,"stroke":102,"style":67},"90","9","#b91c1c",[51,104,106],{"x":90,"y":105,"style":71,"fill":102,"textAnchor":57},"64","A: 0.6 km straight, 4.1 km road",[88,108],{"cx":109,"cy":110,"r":101,"fill":111,"stroke":111,"style":67},"120","224","#15803d",[51,113,116],{"x":109,"y":114,"style":71,"fill":115,"textAnchor":57},"254","#166534","B: 1.4 km straight, 1.8 km road",[118,119],"line",{"x1":90,"y1":120,"x2":90,"y2":121,"stroke":102,"style":122},"210","100","stroke-width:1.5;stroke-dasharray:4 3",[124,125],"polyline",{"points":126,"fill":127,"stroke":102,"style":128},"300,220 620,220 620,90 310,90","none","stroke-width:1.5;stroke-dasharray:2 3",[118,130],{"x1":131,"y1":132,"x2":133,"y2":110,"stroke":111,"style":134},"292","222","130","stroke-width:2",[136,137,139],"h2",{"id":138},"prerequisites","Prerequisites",[141,142,143,147,155,158],"ul",{},[144,145,146],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series.",[144,148,149,150,154],{},"A road network as a line layer in a projected CRS, with connectivity checked as in ",[21,151,153],{"href":152},"\u002Fspatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002Fcheck-line-network-connectivity-pyqgis\u002F","checking line network connectivity",".",[144,156,157],{},"A facilities layer (stations, clinics, depots) and a locations layer (addresses, incidents, customers).",[144,159,160,161,154],{},"Familiarity with graph building from ",[21,162,164],{"href":163},"\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fbuild-network-graph-with-qgsgraphbuilder\u002F","building a network graph with QgsGraphBuilder",[136,166,168],{"id":167},"build-the-graph-with-every-point-tied-in","Build the graph with every point tied in",[14,170,171],{},"Facilities and locations must become graph vertices. Passing all of them to the director as tie points snaps each onto the network once.",[173,174,179],"pre",{"className":175,"code":176,"language":177,"meta":178,"style":178},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject, QgsVectorLayer\nfrom qgis.analysis import (QgsVectorLayerDirector, QgsNetworkDistanceStrategy,\n                           QgsGraphBuilder, QgsGraphAnalyzer)\n\nroads = QgsProject.instance().mapLayersByName(\"roads\")[0]\nstations = QgsProject.instance().mapLayersByName(\"fire_stations\")[0]\naddresses = QgsProject.instance().mapLayersByName(\"addresses\")[0]\n\ndirector = QgsVectorLayerDirector(roads, -1, \"\", \"\", \"\", QgsVectorLayerDirector.DirectionBoth)\ndirector.addStrategy(QgsNetworkDistanceStrategy())\nbuilder = QgsGraphBuilder(roads.crs(), True, 1.0)\n\nst_pts = [(f[\"station_id\"], f.geometry().asPoint()) for f in stations.getFeatures()]\nad_pts = [(f.id(), f.geometry().asPoint()) for f in addresses.getFeatures()]\ntied = director.makeGraph(builder, [p for _, p in st_pts] + [p for _, p in ad_pts])\ngraph = builder.graph()\n\nst_vertices = [(sid, graph.findVertex(tied[i])) for i, (sid, _) in enumerate(st_pts)]\noffset = len(st_pts)\nad_vertices = [(fid, graph.findVertex(tied[offset + i])) for i, (fid, _) in enumerate(ad_pts)]\nprint(graph.vertexCount(), \"vertices;\", len(st_vertices), \"stations;\", len(ad_vertices), \"addresses\")\n","python","",[180,181,182,200,213,219,226,251,270,289,294,328,334,356,361,390,410,446,457,462,486,500,528],"code",{"__ignoreMap":178},[183,184,186,190,194,197],"span",{"class":118,"line":185},1,[183,187,189],{"class":188},"snl16","from",[183,191,193],{"class":192},"s95oV"," qgis.core ",[183,195,196],{"class":188},"import",[183,198,199],{"class":192}," QgsProject, QgsVectorLayer\n",[183,201,203,205,208,210],{"class":118,"line":202},2,[183,204,189],{"class":188},[183,206,207],{"class":192}," qgis.analysis ",[183,209,196],{"class":188},[183,211,212],{"class":192}," (QgsVectorLayerDirector, QgsNetworkDistanceStrategy,\n",[183,214,216],{"class":118,"line":215},3,[183,217,218],{"class":192},"                           QgsGraphBuilder, QgsGraphAnalyzer)\n",[183,220,222],{"class":118,"line":221},4,[183,223,225],{"emptyLinePlaceholder":224},true,"\n",[183,227,229,232,235,238,242,245,248],{"class":118,"line":228},5,[183,230,231],{"class":192},"roads ",[183,233,234],{"class":188},"=",[183,236,237],{"class":192}," QgsProject.instance().mapLayersByName(",[183,239,241],{"class":240},"sU2Wk","\"roads\"",[183,243,244],{"class":192},")[",[183,246,46],{"class":247},"sDLfK",[183,249,250],{"class":192},"]\n",[183,252,254,257,259,261,264,266,268],{"class":118,"line":253},6,[183,255,256],{"class":192},"stations ",[183,258,234],{"class":188},[183,260,237],{"class":192},[183,262,263],{"class":240},"\"fire_stations\"",[183,265,244],{"class":192},[183,267,46],{"class":247},[183,269,250],{"class":192},[183,271,273,276,278,280,283,285,287],{"class":118,"line":272},7,[183,274,275],{"class":192},"addresses ",[183,277,234],{"class":188},[183,279,237],{"class":192},[183,281,282],{"class":240},"\"addresses\"",[183,284,244],{"class":192},[183,286,46],{"class":247},[183,288,250],{"class":192},[183,290,292],{"class":118,"line":291},8,[183,293,225],{"emptyLinePlaceholder":224},[183,295,297,300,302,305,308,311,314,317,319,321,323,325],{"class":118,"line":296},9,[183,298,299],{"class":192},"director ",[183,301,234],{"class":188},[183,303,304],{"class":192}," QgsVectorLayerDirector(roads, ",[183,306,307],{"class":188},"-",[183,309,310],{"class":247},"1",[183,312,313],{"class":192},", ",[183,315,316],{"class":240},"\"\"",[183,318,313],{"class":192},[183,320,316],{"class":240},[183,322,313],{"class":192},[183,324,316],{"class":240},[183,326,327],{"class":192},", QgsVectorLayerDirector.DirectionBoth)\n",[183,329,331],{"class":118,"line":330},10,[183,332,333],{"class":192},"director.addStrategy(QgsNetworkDistanceStrategy())\n",[183,335,337,340,342,345,348,350,353],{"class":118,"line":336},11,[183,338,339],{"class":192},"builder ",[183,341,234],{"class":188},[183,343,344],{"class":192}," QgsGraphBuilder(roads.crs(), ",[183,346,347],{"class":247},"True",[183,349,313],{"class":192},[183,351,352],{"class":247},"1.0",[183,354,355],{"class":192},")\n",[183,357,359],{"class":118,"line":358},12,[183,360,225],{"emptyLinePlaceholder":224},[183,362,364,367,369,372,375,378,381,384,387],{"class":118,"line":363},13,[183,365,366],{"class":192},"st_pts ",[183,368,234],{"class":188},[183,370,371],{"class":192}," [(f[",[183,373,374],{"class":240},"\"station_id\"",[183,376,377],{"class":192},"], f.geometry().asPoint()) ",[183,379,380],{"class":188},"for",[183,382,383],{"class":192}," f ",[183,385,386],{"class":188},"in",[183,388,389],{"class":192}," stations.getFeatures()]\n",[183,391,393,396,398,401,403,405,407],{"class":118,"line":392},14,[183,394,395],{"class":192},"ad_pts ",[183,397,234],{"class":188},[183,399,400],{"class":192}," [(f.id(), f.geometry().asPoint()) ",[183,402,380],{"class":188},[183,404,383],{"class":192},[183,406,386],{"class":188},[183,408,409],{"class":192}," addresses.getFeatures()]\n",[183,411,413,416,418,421,423,426,428,431,434,437,439,441,443],{"class":118,"line":412},15,[183,414,415],{"class":192},"tied ",[183,417,234],{"class":188},[183,419,420],{"class":192}," director.makeGraph(builder, [p ",[183,422,380],{"class":188},[183,424,425],{"class":192}," _, p ",[183,427,386],{"class":188},[183,429,430],{"class":192}," st_pts] ",[183,432,433],{"class":188},"+",[183,435,436],{"class":192}," [p ",[183,438,380],{"class":188},[183,440,425],{"class":192},[183,442,386],{"class":188},[183,444,445],{"class":192}," ad_pts])\n",[183,447,449,452,454],{"class":118,"line":448},16,[183,450,451],{"class":192},"graph ",[183,453,234],{"class":188},[183,455,456],{"class":192}," builder.graph()\n",[183,458,460],{"class":118,"line":459},17,[183,461,225],{"emptyLinePlaceholder":224},[183,463,465,468,470,473,475,478,480,483],{"class":118,"line":464},18,[183,466,467],{"class":192},"st_vertices ",[183,469,234],{"class":188},[183,471,472],{"class":192}," [(sid, graph.findVertex(tied[i])) ",[183,474,380],{"class":188},[183,476,477],{"class":192}," i, (sid, _) ",[183,479,386],{"class":188},[183,481,482],{"class":247}," enumerate",[183,484,485],{"class":192},"(st_pts)]\n",[183,487,489,492,494,497],{"class":118,"line":488},19,[183,490,491],{"class":192},"offset ",[183,493,234],{"class":188},[183,495,496],{"class":247}," len",[183,498,499],{"class":192},"(st_pts)\n",[183,501,503,506,508,511,513,516,518,521,523,525],{"class":118,"line":502},20,[183,504,505],{"class":192},"ad_vertices ",[183,507,234],{"class":188},[183,509,510],{"class":192}," [(fid, graph.findVertex(tied[offset ",[183,512,433],{"class":188},[183,514,515],{"class":192}," i])) ",[183,517,380],{"class":188},[183,519,520],{"class":192}," i, (fid, _) ",[183,522,386],{"class":188},[183,524,482],{"class":247},[183,526,527],{"class":192},"(ad_pts)]\n",[183,529,531,534,537,540,542,545,548,551,553,555,558,560],{"class":118,"line":530},21,[183,532,533],{"class":247},"print",[183,535,536],{"class":192},"(graph.vertexCount(), ",[183,538,539],{"class":240},"\"vertices;\"",[183,541,313],{"class":192},[183,543,544],{"class":247},"len",[183,546,547],{"class":192},"(st_vertices), ",[183,549,550],{"class":240},"\"stations;\"",[183,552,313],{"class":192},[183,554,544],{"class":247},[183,556,557],{"class":192},"(ad_vertices), ",[183,559,282],{"class":240},[183,561,355],{"class":192},[14,563,564,568,569,572,573,577],{},[565,566,567],"strong",{},"Breakdown:"," ",[180,570,571],{},"makeGraph"," returns the tied (snapped) positions in the same order as the input points, so the first entries belong to stations and the rest to addresses; keeping that order straight is the main bookkeeping in this workflow. The topology tolerance of one metre merges nearly coincident road ends. Distance strategy makes edge cost equal to length; for travel time, use a speed strategy as in ",[21,574,576],{"href":575},"\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fadd-speed-and-travel-cost-to-network-pyqgis\u002F","adding speed and travel cost to a network",". Tying every address into the graph is fine for thousands of points; for hundreds of thousands, snap addresses to the nearest graph vertex instead.",[136,579,581],{"id":580},"run-one-tree-per-facility","Run one tree per facility",[14,583,584,585,589],{},"Routing from each address to each station would mean addresses × stations shortest paths. Dijkstra from a station computes the cost to ",[586,587,588],"em",{},"every"," vertex in one run, so one run per station covers all addresses.",[14,591,592],{},[29,593,596,599,602,605,620,623,629,635,639,643,647,652,656,660,664,668,672,675],{"viewBox":594,"role":32,"ariaLabel":595,"xmlns":34},"0 0 760 196","One Dijkstra run per station giving costs to all addresses, with each address assigned to the station of lowest cost",[36,597,598],{},"One shortest-path tree per facility",[40,600,601],{},"For each of a few stations, Dijkstra computes the cost from that station to every vertex of the graph in one run. The address vertices' costs are read from each tree, and for every address the station with the lowest cost is kept. Ten stations and fifty thousand addresses need ten runs, not half a million routes.",[44,603],{"x":46,"y":46,"width":47,"height":604,"fill":49},"196",[606,607,608],"defs",{},[609,610,616],"marker",{"id":611,"viewBox":612,"refX":92,"refY":613,"markerWidth":614,"markerHeight":614,"orient":615},"nfTreesArrow","0 0 10 10","5","7","auto-start-reverse",[617,618],"path",{"d":619,"fill":96},"M0 0 L10 5 L0 10 z",[51,621,622],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Facilities × 1 run, not facilities × locations",[44,624],{"x":625,"y":626,"width":627,"height":109,"rx":92,"fill":628,"stroke":79,"style":134},"24","60","200","#fffdf7",[51,630,634],{"x":631,"y":632,"style":633,"fill":56,"textAnchor":57},"124","99.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","station s",[51,636,638],{"x":631,"y":637,"style":71,"fill":79,"textAnchor":57},"123.78","Dijkstra",[51,640,642],{"x":631,"y":641,"style":71,"fill":79,"textAnchor":57},"147.78","cost to all vertices",[118,644],{"x1":110,"y1":109,"x2":645,"y2":109,"stroke":96,"style":646},"266","stroke-width:1.8;marker-end:url(#nfTreesArrow)",[44,648],{"x":649,"y":626,"width":91,"height":109,"rx":92,"fill":650,"stroke":651,"style":134},"270","#eef7f4","#0f766e",[51,653,655],{"x":53,"y":654,"style":633,"fill":651,"textAnchor":57},"111.78","read address costs",[51,657,659],{"x":53,"y":658,"style":71,"fill":79,"textAnchor":57},"135.78","cost[v] for each",[118,661],{"x1":662,"y1":109,"x2":663,"y2":109,"stroke":96,"style":646},"490","532",[44,665],{"x":666,"y":626,"width":627,"height":109,"rx":92,"fill":667,"stroke":111,"style":134},"536","#e8efe6",[51,669,671],{"x":670,"y":632,"style":633,"fill":115,"textAnchor":57},"636","keep minimum",[51,673,674],{"x":670,"y":637,"style":71,"fill":79,"textAnchor":57},"best station",[51,676,677],{"x":670,"y":641,"style":71,"fill":79,"textAnchor":57},"best cost",[173,679,681],{"className":175,"code":680,"language":177,"meta":178,"style":178},"import math\n\nbest = {fid: (None, math.inf) for fid, _ in ad_vertices}\nfor sid, sv in st_vertices:\n    tree, cost = QgsGraphAnalyzer.dijkstra(graph, sv, 0)\n    for fid, av in ad_vertices:\n        c = cost[av]\n        if tree[av] != -1 or av == sv:          # reachable\n            if c \u003C best[fid][1]:\n                best[fid] = (sid, c)\n\nunreached = [fid for fid, (sid, c) in best.items() if sid is None]\nprint(len(unreached), \"addresses not reachable from any station\")\n",[180,682,683,690,694,720,732,746,759,769,801,820,830,834,868],{"__ignoreMap":178},[183,684,685,687],{"class":118,"line":185},[183,686,196],{"class":188},[183,688,689],{"class":192}," math\n",[183,691,692],{"class":118,"line":202},[183,693,225],{"emptyLinePlaceholder":224},[183,695,696,699,701,704,707,710,712,715,717],{"class":118,"line":215},[183,697,698],{"class":192},"best ",[183,700,234],{"class":188},[183,702,703],{"class":192}," {fid: (",[183,705,706],{"class":247},"None",[183,708,709],{"class":192},", math.inf) ",[183,711,380],{"class":188},[183,713,714],{"class":192}," fid, _ ",[183,716,386],{"class":188},[183,718,719],{"class":192}," ad_vertices}\n",[183,721,722,724,727,729],{"class":118,"line":221},[183,723,380],{"class":188},[183,725,726],{"class":192}," sid, sv ",[183,728,386],{"class":188},[183,730,731],{"class":192}," st_vertices:\n",[183,733,734,737,739,742,744],{"class":118,"line":228},[183,735,736],{"class":192},"    tree, cost ",[183,738,234],{"class":188},[183,740,741],{"class":192}," QgsGraphAnalyzer.dijkstra(graph, sv, ",[183,743,46],{"class":247},[183,745,355],{"class":192},[183,747,748,751,754,756],{"class":118,"line":253},[183,749,750],{"class":188},"    for",[183,752,753],{"class":192}," fid, av ",[183,755,386],{"class":188},[183,757,758],{"class":192}," ad_vertices:\n",[183,760,761,764,766],{"class":118,"line":272},[183,762,763],{"class":192},"        c ",[183,765,234],{"class":188},[183,767,768],{"class":192}," cost[av]\n",[183,770,771,774,777,780,783,785,788,791,794,797],{"class":118,"line":291},[183,772,773],{"class":188},"        if",[183,775,776],{"class":192}," tree[av] ",[183,778,779],{"class":188},"!=",[183,781,782],{"class":188}," -",[183,784,310],{"class":247},[183,786,787],{"class":188}," or",[183,789,790],{"class":192}," av ",[183,792,793],{"class":188},"==",[183,795,796],{"class":192}," sv:          ",[183,798,800],{"class":799},"sjoCn","# reachable\n",[183,802,803,806,809,812,815,817],{"class":118,"line":296},[183,804,805],{"class":188},"            if",[183,807,808],{"class":192}," c ",[183,810,811],{"class":188},"\u003C",[183,813,814],{"class":192}," best[fid][",[183,816,310],{"class":247},[183,818,819],{"class":192},"]:\n",[183,821,822,825,827],{"class":118,"line":330},[183,823,824],{"class":192},"                best[fid] ",[183,826,234],{"class":188},[183,828,829],{"class":192}," (sid, c)\n",[183,831,832],{"class":118,"line":336},[183,833,225],{"emptyLinePlaceholder":224},[183,835,836,839,841,844,846,849,851,854,857,860,863,866],{"class":118,"line":358},[183,837,838],{"class":192},"unreached ",[183,840,234],{"class":188},[183,842,843],{"class":192}," [fid ",[183,845,380],{"class":188},[183,847,848],{"class":192}," fid, (sid, c) ",[183,850,386],{"class":188},[183,852,853],{"class":192}," best.items() ",[183,855,856],{"class":188},"if",[183,858,859],{"class":192}," sid ",[183,861,862],{"class":188},"is",[183,864,865],{"class":247}," None",[183,867,250],{"class":192},[183,869,870,872,875,877,880,883],{"class":118,"line":363},[183,871,533],{"class":247},[183,873,874],{"class":192},"(",[183,876,544],{"class":247},[183,878,879],{"class":192},"(unreached), ",[183,881,882],{"class":240},"\"addresses not reachable from any station\"",[183,884,355],{"class":192},[14,886,887,568,889,892],{},[565,888,567],{},[180,890,891],{},"dijkstra"," returns two lists indexed by vertex: the incoming edge of the shortest-path tree and the cost from the start. A vertex is reachable if it has an incoming edge (or is the start itself); unreachable vertices keep an infinite or meaningless cost and must be excluded. Updating the best station per address as each tree is computed keeps memory low — only one tree exists at a time. Ten stations and fifty thousand addresses take ten Dijkstra runs, typically seconds.",[136,894,896],{"id":895},"write-the-assignment-to-a-layer","Write the assignment to a layer",[14,898,899],{},"The result belongs on the addresses: the assigned station and the network distance, ready to map, count and report.",[173,901,903],{"className":175,"code":902,"language":177,"meta":178,"style":178},"from qgis.core import QgsField, edit\nfrom qgis.PyQt.QtCore import QVariant\n\nprov = addresses.dataProvider()\nfor name, qtype in ((\"nearest_station\", QVariant.String), (\"road_km\", QVariant.Double)):\n    if addresses.fields().indexOf(name) \u003C 0:\n        prov.addAttributes([QgsField(name, qtype)])\naddresses.updateFields()\ni_st, i_km = addresses.fields().indexOf(\"nearest_station\"), addresses.fields().indexOf(\"road_km\")\n\nchanges = {fid: {i_st: sid, i_km: (round(c \u002F 1000, 3) if sid else None)}\n           for fid, (sid, c) in best.items()}\nprov.changeAttributeValues(changes)\nprint(\"assigned\", sum(1 for s, _ in best.values() if s), \"addresses\")\n",[180,904,905,916,928,932,942,966,982,987,992,1011,1015,1057,1069,1074],{"__ignoreMap":178},[183,906,907,909,911,913],{"class":118,"line":185},[183,908,189],{"class":188},[183,910,193],{"class":192},[183,912,196],{"class":188},[183,914,915],{"class":192}," QgsField, edit\n",[183,917,918,920,923,925],{"class":118,"line":202},[183,919,189],{"class":188},[183,921,922],{"class":192}," qgis.PyQt.QtCore ",[183,924,196],{"class":188},[183,926,927],{"class":192}," QVariant\n",[183,929,930],{"class":118,"line":215},[183,931,225],{"emptyLinePlaceholder":224},[183,933,934,937,939],{"class":118,"line":221},[183,935,936],{"class":192},"prov ",[183,938,234],{"class":188},[183,940,941],{"class":192}," addresses.dataProvider()\n",[183,943,944,946,949,951,954,957,960,963],{"class":118,"line":228},[183,945,380],{"class":188},[183,947,948],{"class":192}," name, qtype ",[183,950,386],{"class":188},[183,952,953],{"class":192}," ((",[183,955,956],{"class":240},"\"nearest_station\"",[183,958,959],{"class":192},", QVariant.String), (",[183,961,962],{"class":240},"\"road_km\"",[183,964,965],{"class":192},", QVariant.Double)):\n",[183,967,968,971,974,976,979],{"class":118,"line":253},[183,969,970],{"class":188},"    if",[183,972,973],{"class":192}," addresses.fields().indexOf(name) ",[183,975,811],{"class":188},[183,977,978],{"class":247}," 0",[183,980,981],{"class":192},":\n",[183,983,984],{"class":118,"line":272},[183,985,986],{"class":192},"        prov.addAttributes([QgsField(name, qtype)])\n",[183,988,989],{"class":118,"line":291},[183,990,991],{"class":192},"addresses.updateFields()\n",[183,993,994,997,999,1002,1004,1007,1009],{"class":118,"line":296},[183,995,996],{"class":192},"i_st, i_km ",[183,998,234],{"class":188},[183,1000,1001],{"class":192}," addresses.fields().indexOf(",[183,1003,956],{"class":240},[183,1005,1006],{"class":192},"), addresses.fields().indexOf(",[183,1008,962],{"class":240},[183,1010,355],{"class":192},[183,1012,1013],{"class":118,"line":330},[183,1014,225],{"emptyLinePlaceholder":224},[183,1016,1017,1020,1022,1025,1028,1031,1034,1037,1039,1042,1045,1047,1049,1052,1054],{"class":118,"line":336},[183,1018,1019],{"class":192},"changes ",[183,1021,234],{"class":188},[183,1023,1024],{"class":192}," {fid: {i_st: sid, i_km: (",[183,1026,1027],{"class":247},"round",[183,1029,1030],{"class":192},"(c ",[183,1032,1033],{"class":188},"\u002F",[183,1035,1036],{"class":247}," 1000",[183,1038,313],{"class":192},[183,1040,1041],{"class":247},"3",[183,1043,1044],{"class":192},") ",[183,1046,856],{"class":188},[183,1048,859],{"class":192},[183,1050,1051],{"class":188},"else",[183,1053,865],{"class":247},[183,1055,1056],{"class":192},")}\n",[183,1058,1059,1062,1064,1066],{"class":118,"line":358},[183,1060,1061],{"class":188},"           for",[183,1063,848],{"class":192},[183,1065,386],{"class":188},[183,1067,1068],{"class":192}," best.items()}\n",[183,1070,1071],{"class":118,"line":363},[183,1072,1073],{"class":192},"prov.changeAttributeValues(changes)\n",[183,1075,1076,1078,1080,1083,1085,1088,1090,1092,1095,1098,1100,1103,1105,1108,1110],{"class":118,"line":392},[183,1077,533],{"class":247},[183,1079,874],{"class":192},[183,1081,1082],{"class":240},"\"assigned\"",[183,1084,313],{"class":192},[183,1086,1087],{"class":247},"sum",[183,1089,874],{"class":192},[183,1091,310],{"class":247},[183,1093,1094],{"class":188}," for",[183,1096,1097],{"class":192}," s, _ ",[183,1099,386],{"class":188},[183,1101,1102],{"class":192}," best.values() ",[183,1104,856],{"class":188},[183,1106,1107],{"class":192}," s), ",[183,1109,282],{"class":240},[183,1111,355],{"class":192},[14,1113,1114,1116,1117,1120,1121,1125],{},[565,1115,567],{}," One provider call writes both fields for every address. Unreachable addresses get NULL rather than a misleading station. Styling addresses by ",[180,1118,1119],{},"nearest_station"," with a categorized renderer shows each station's service area as a coloured point pattern; dissolving Voronoi-like regions from those points, or ",[21,1122,1124],{"href":1123},"\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fcalculate-service-areas-pyqgis\u002F","calculating service areas"," per station, turns it into polygons.",[136,1127,1129],{"id":1128},"compare-with-straight-line-assignment","Compare with straight-line assignment",[14,1131,1132],{},"The interesting addresses are those where the network answer differs from the straight-line answer — they show where barriers shape access.",[173,1134,1136],{"className":175,"code":1135,"language":177,"meta":178,"style":178},"from qgis.core import QgsSpatialIndex\n\nst_index = QgsSpatialIndex()\nst_geoms = {}\nfor f in stations.getFeatures():\n    st_index.addFeature(f)\n    st_geoms[f.id()] = f[\"station_id\"]\n\ndiffers = 0\nfor f in addresses.getFeatures():\n    nearest_fid = st_index.nearestNeighbor(f.geometry().asPoint(), 1)[0]\n    if st_geoms[nearest_fid] != best[f.id()][0]:\n        differs += 1\nprint(f\"{differs} addresses ({differs \u002F addresses.featureCount():.1%}) are served by a \"\n      \"different station than the straight-line nearest\")\n",[180,1137,1138,1149,1153,1163,1173,1184,1189,1203,1207,1217,1228,1246,1262,1273,1314],{"__ignoreMap":178},[183,1139,1140,1142,1144,1146],{"class":118,"line":185},[183,1141,189],{"class":188},[183,1143,193],{"class":192},[183,1145,196],{"class":188},[183,1147,1148],{"class":192}," QgsSpatialIndex\n",[183,1150,1151],{"class":118,"line":202},[183,1152,225],{"emptyLinePlaceholder":224},[183,1154,1155,1158,1160],{"class":118,"line":215},[183,1156,1157],{"class":192},"st_index ",[183,1159,234],{"class":188},[183,1161,1162],{"class":192}," QgsSpatialIndex()\n",[183,1164,1165,1168,1170],{"class":118,"line":221},[183,1166,1167],{"class":192},"st_geoms ",[183,1169,234],{"class":188},[183,1171,1172],{"class":192}," {}\n",[183,1174,1175,1177,1179,1181],{"class":118,"line":228},[183,1176,380],{"class":188},[183,1178,383],{"class":192},[183,1180,386],{"class":188},[183,1182,1183],{"class":192}," stations.getFeatures():\n",[183,1185,1186],{"class":118,"line":253},[183,1187,1188],{"class":192},"    st_index.addFeature(f)\n",[183,1190,1191,1194,1196,1199,1201],{"class":118,"line":272},[183,1192,1193],{"class":192},"    st_geoms[f.id()] ",[183,1195,234],{"class":188},[183,1197,1198],{"class":192}," f[",[183,1200,374],{"class":240},[183,1202,250],{"class":192},[183,1204,1205],{"class":118,"line":291},[183,1206,225],{"emptyLinePlaceholder":224},[183,1208,1209,1212,1214],{"class":118,"line":296},[183,1210,1211],{"class":192},"differs ",[183,1213,234],{"class":188},[183,1215,1216],{"class":247}," 0\n",[183,1218,1219,1221,1223,1225],{"class":118,"line":330},[183,1220,380],{"class":188},[183,1222,383],{"class":192},[183,1224,386],{"class":188},[183,1226,1227],{"class":192}," addresses.getFeatures():\n",[183,1229,1230,1233,1235,1238,1240,1242,1244],{"class":118,"line":336},[183,1231,1232],{"class":192},"    nearest_fid ",[183,1234,234],{"class":188},[183,1236,1237],{"class":192}," st_index.nearestNeighbor(f.geometry().asPoint(), ",[183,1239,310],{"class":247},[183,1241,244],{"class":192},[183,1243,46],{"class":247},[183,1245,250],{"class":192},[183,1247,1248,1250,1253,1255,1258,1260],{"class":118,"line":358},[183,1249,970],{"class":188},[183,1251,1252],{"class":192}," st_geoms[nearest_fid] ",[183,1254,779],{"class":188},[183,1256,1257],{"class":192}," best[f.id()][",[183,1259,46],{"class":247},[183,1261,819],{"class":192},[183,1263,1264,1267,1270],{"class":118,"line":363},[183,1265,1266],{"class":192},"        differs ",[183,1268,1269],{"class":188},"+=",[183,1271,1272],{"class":247}," 1\n",[183,1274,1275,1277,1279,1282,1285,1288,1291,1294,1297,1299,1301,1303,1306,1309,1311],{"class":118,"line":392},[183,1276,533],{"class":247},[183,1278,874],{"class":192},[183,1280,1281],{"class":188},"f",[183,1283,1284],{"class":240},"\"",[183,1286,1287],{"class":247},"{",[183,1289,1290],{"class":192},"differs",[183,1292,1293],{"class":247},"}",[183,1295,1296],{"class":240}," addresses (",[183,1298,1287],{"class":247},[183,1300,1211],{"class":192},[183,1302,1033],{"class":188},[183,1304,1305],{"class":192}," addresses.featureCount()",[183,1307,1308],{"class":188},":.1%",[183,1310,1293],{"class":247},[183,1312,1313],{"class":240},") are served by a \"\n",[183,1315,1316,1319],{"class":118,"line":412},[183,1317,1318],{"class":240},"      \"different station than the straight-line nearest\"",[183,1320,355],{"class":192},[14,1322,1323,1325],{},[565,1324,567],{}," A spatial index finds the straight-line nearest station quickly; comparing it with the network result counts addresses where geometry misleads. Mapping them usually reveals rivers, railways and motorways as clear boundaries. The share is a useful headline for anyone who suggests that straight-line distance is \"good enough\".",[136,1327,1329],{"id":1328},"report-coverage-against-a-standard","Report coverage against a standard",[14,1331,1332],{},"Service planning is usually judged against a standard: 90 % of addresses within 8 minutes of a fire station, every pupil within 3 km of a primary school. With the best cost per address computed, coverage is a count.",[173,1334,1336],{"className":175,"code":1335,"language":177,"meta":178,"style":178},"STANDARD_KM = 3.0\nwithin = [fid for fid, (sid, c) in best.items() if sid and c \u002F 1000 \u003C= STANDARD_KM]\nshare = len(within) \u002F max(len(best), 1)\nprint(f\"{share:.1%} of addresses within {STANDARD_KM} km road distance of a station\")\n\nby_station = {}\nfor fid, (sid, c) in best.items():\n    if sid:\n        by_station.setdefault(sid, []).append(c \u002F 1000)\nfor sid, d in sorted(by_station.items()):\n    d.sort()\n    print(f\"{sid:\u003C10} {len(d):>6} addresses, median {d[len(d) \u002F\u002F 2]:.1f} km, \"\n          f\"90th pct {d[int(len(d) * 0.9)]:.1f} km\")\n",[180,1337,1338,1349,1387,1415,1445,1449,1458,1469,1476,1487,1502,1507,1569],{"__ignoreMap":178},[183,1339,1340,1343,1346],{"class":118,"line":185},[183,1341,1342],{"class":247},"STANDARD_KM",[183,1344,1345],{"class":188}," =",[183,1347,1348],{"class":247}," 3.0\n",[183,1350,1351,1354,1356,1358,1360,1362,1364,1366,1368,1370,1373,1375,1377,1379,1382,1385],{"class":118,"line":202},[183,1352,1353],{"class":192},"within ",[183,1355,234],{"class":188},[183,1357,843],{"class":192},[183,1359,380],{"class":188},[183,1361,848],{"class":192},[183,1363,386],{"class":188},[183,1365,853],{"class":192},[183,1367,856],{"class":188},[183,1369,859],{"class":192},[183,1371,1372],{"class":188},"and",[183,1374,808],{"class":192},[183,1376,1033],{"class":188},[183,1378,1036],{"class":247},[183,1380,1381],{"class":188}," \u003C=",[183,1383,1384],{"class":247}," STANDARD_KM",[183,1386,250],{"class":192},[183,1388,1389,1392,1394,1396,1399,1401,1404,1406,1408,1411,1413],{"class":118,"line":215},[183,1390,1391],{"class":192},"share ",[183,1393,234],{"class":188},[183,1395,496],{"class":247},[183,1397,1398],{"class":192},"(within) ",[183,1400,1033],{"class":188},[183,1402,1403],{"class":247}," max",[183,1405,874],{"class":192},[183,1407,544],{"class":247},[183,1409,1410],{"class":192},"(best), ",[183,1412,310],{"class":247},[183,1414,355],{"class":192},[183,1416,1417,1419,1421,1423,1425,1427,1430,1432,1434,1437,1440,1443],{"class":118,"line":221},[183,1418,533],{"class":247},[183,1420,874],{"class":192},[183,1422,1281],{"class":188},[183,1424,1284],{"class":240},[183,1426,1287],{"class":247},[183,1428,1429],{"class":192},"share",[183,1431,1308],{"class":188},[183,1433,1293],{"class":247},[183,1435,1436],{"class":240}," of addresses within ",[183,1438,1439],{"class":247},"{STANDARD_KM}",[183,1441,1442],{"class":240}," km road distance of a station\"",[183,1444,355],{"class":192},[183,1446,1447],{"class":118,"line":228},[183,1448,225],{"emptyLinePlaceholder":224},[183,1450,1451,1454,1456],{"class":118,"line":253},[183,1452,1453],{"class":192},"by_station ",[183,1455,234],{"class":188},[183,1457,1172],{"class":192},[183,1459,1460,1462,1464,1466],{"class":118,"line":272},[183,1461,380],{"class":188},[183,1463,848],{"class":192},[183,1465,386],{"class":188},[183,1467,1468],{"class":192}," best.items():\n",[183,1470,1471,1473],{"class":118,"line":291},[183,1472,970],{"class":188},[183,1474,1475],{"class":192}," sid:\n",[183,1477,1478,1481,1483,1485],{"class":118,"line":296},[183,1479,1480],{"class":192},"        by_station.setdefault(sid, []).append(c ",[183,1482,1033],{"class":188},[183,1484,1036],{"class":247},[183,1486,355],{"class":192},[183,1488,1489,1491,1494,1496,1499],{"class":118,"line":330},[183,1490,380],{"class":188},[183,1492,1493],{"class":192}," sid, d ",[183,1495,386],{"class":188},[183,1497,1498],{"class":247}," sorted",[183,1500,1501],{"class":192},"(by_station.items()):\n",[183,1503,1504],{"class":118,"line":336},[183,1505,1506],{"class":192},"    d.sort()\n",[183,1508,1509,1512,1514,1516,1518,1520,1523,1526,1528,1531,1534,1537,1539,1542,1544,1547,1549,1552,1555,1558,1561,1564,1566],{"class":118,"line":358},[183,1510,1511],{"class":247},"    print",[183,1513,874],{"class":192},[183,1515,1281],{"class":188},[183,1517,1284],{"class":240},[183,1519,1287],{"class":247},[183,1521,1522],{"class":192},"sid",[183,1524,1525],{"class":188},":\u003C10",[183,1527,1293],{"class":247},[183,1529,1530],{"class":247}," {len",[183,1532,1533],{"class":192},"(d)",[183,1535,1536],{"class":188},":>6",[183,1538,1293],{"class":247},[183,1540,1541],{"class":240}," addresses, median ",[183,1543,1287],{"class":247},[183,1545,1546],{"class":192},"d[",[183,1548,544],{"class":247},[183,1550,1551],{"class":192},"(d) ",[183,1553,1554],{"class":188},"\u002F\u002F",[183,1556,1557],{"class":247}," 2",[183,1559,1560],{"class":192},"]",[183,1562,1563],{"class":188},":.1f",[183,1565,1293],{"class":247},[183,1567,1568],{"class":240}," km, \"\n",[183,1570,1571,1574,1577,1579,1581,1584,1586,1588,1590,1593,1596,1599,1601,1603,1606],{"class":118,"line":363},[183,1572,1573],{"class":188},"          f",[183,1575,1576],{"class":240},"\"90th pct ",[183,1578,1287],{"class":247},[183,1580,1546],{"class":192},[183,1582,1583],{"class":247},"int",[183,1585,874],{"class":192},[183,1587,544],{"class":247},[183,1589,1551],{"class":192},[183,1591,1592],{"class":188},"*",[183,1594,1595],{"class":247}," 0.9",[183,1597,1598],{"class":192},")]",[183,1600,1563],{"class":188},[183,1602,1293],{"class":247},[183,1604,1605],{"class":240}," km\"",[183,1607,355],{"class":192},[14,1609,1610,1612],{},[565,1611,567],{}," The share of addresses within the standard is the headline figure; per-station counts and distance percentiles show which stations carry most of the load and which serve a long tail of remote addresses. Mapping the addresses outside the standard identifies where a new facility would help most — they are the natural candidates to feed into a location-allocation exercise. For time-based standards, build the graph with a speed strategy so costs are already in seconds.",[136,1614,1616],{"id":1615},"respect-facility-capacity","Respect facility capacity",[14,1618,1619],{},"Nearest assignment ignores capacity: a school with 300 places may be nearest for 600 pupils. A simple greedy pass assigns locations in order of how much they lose by not getting their first choice.",[14,1621,1622],{},[29,1623,1625,1628,1631,1633,1640,1643,1645,1649,1653,1656,1660,1663,1666,1668,1670,1673],{"viewBox":594,"role":32,"ariaLabel":1624,"xmlns":34},"A greedy capacity-aware assignment ordering locations by regret and giving each its cheapest facility with remaining capacity",[36,1626,1627],{},"Capacity-aware assignment",[40,1629,1630],{},"Each location has a cost to every facility. Locations are sorted by regret, the difference between their best and second-best costs. In that order, each is assigned to its cheapest facility with remaining capacity. Locations that would lose most by moving are placed first, giving a reasonable assignment without an optimisation solver.",[44,1632],{"x":46,"y":46,"width":47,"height":604,"fill":49},[606,1634,1635],{},[609,1636,1638],{"id":1637,"viewBox":612,"refX":92,"refY":613,"markerWidth":614,"markerHeight":614,"orient":615},"nfCapacityArrow",[617,1639],{"d":619,"fill":96},[51,1641,1642],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Assign the most constrained first",[44,1644],{"x":625,"y":626,"width":627,"height":109,"rx":92,"fill":628,"stroke":79,"style":134},[51,1646,1648],{"x":631,"y":1647,"style":633,"fill":56,"textAnchor":57},"110.78","cost matrix",[51,1650,1652],{"x":631,"y":1651,"style":71,"fill":79,"textAnchor":57},"136.78","location × facility",[118,1654],{"x1":110,"y1":109,"x2":645,"y2":109,"stroke":96,"style":1655},"stroke-width:1.8;marker-end:url(#nfCapacityArrow)",[44,1657],{"x":649,"y":626,"width":91,"height":109,"rx":92,"fill":1658,"stroke":1659,"style":134},"#fdf2e2","#b45309",[51,1661,1662],{"x":53,"y":1647,"style":633,"fill":1659,"textAnchor":57},"sort by regret",[51,1664,1665],{"x":53,"y":1651,"style":71,"fill":79,"textAnchor":57},"2nd best − best",[118,1667],{"x1":662,"y1":109,"x2":663,"y2":109,"stroke":96,"style":1655},[44,1669],{"x":666,"y":626,"width":627,"height":109,"rx":92,"fill":667,"stroke":111,"style":134},[51,1671,1672],{"x":670,"y":1647,"style":633,"fill":115,"textAnchor":57},"assign",[51,1674,1675],{"x":670,"y":1651,"style":71,"fill":79,"textAnchor":57},"cheapest with room",[173,1677,1679],{"className":175,"code":1678,"language":177,"meta":178,"style":178},"costs = {fid: {} for fid, _ in ad_vertices}\nfor sid, sv in st_vertices:\n    tree, cost = QgsGraphAnalyzer.dijkstra(graph, sv, 0)\n    for fid, av in ad_vertices:\n        if tree[av] != -1 or av == sv:\n            costs[fid][sid] = cost[av]\n\ncapacity = {f[\"station_id\"]: f[\"capacity\"] for f in stations.getFeatures()}\ndef regret(fid):\n    c = sorted(costs[fid].values())\n    return (c[1] - c[0]) if len(c) > 1 else math.inf\nassigned = {}\nfor fid in sorted(costs, key=regret, reverse=True):\n    for sid, c in sorted(costs[fid].items(), key=lambda kv: kv[1]):\n        if capacity[sid] > 0:\n            assigned[fid] = sid\n            capacity[sid] -= 1\n            break\nprint(len(assigned), \"assigned within capacity\")\n",[180,1680,1681,1699,1709,1721,1731,1752,1761,1765,1795,1807,1819,1860,1869,1902,1929,1942,1952,1962,1967],{"__ignoreMap":178},[183,1682,1683,1686,1688,1691,1693,1695,1697],{"class":118,"line":185},[183,1684,1685],{"class":192},"costs ",[183,1687,234],{"class":188},[183,1689,1690],{"class":192}," {fid: {} ",[183,1692,380],{"class":188},[183,1694,714],{"class":192},[183,1696,386],{"class":188},[183,1698,719],{"class":192},[183,1700,1701,1703,1705,1707],{"class":118,"line":202},[183,1702,380],{"class":188},[183,1704,726],{"class":192},[183,1706,386],{"class":188},[183,1708,731],{"class":192},[183,1710,1711,1713,1715,1717,1719],{"class":118,"line":215},[183,1712,736],{"class":192},[183,1714,234],{"class":188},[183,1716,741],{"class":192},[183,1718,46],{"class":247},[183,1720,355],{"class":192},[183,1722,1723,1725,1727,1729],{"class":118,"line":221},[183,1724,750],{"class":188},[183,1726,753],{"class":192},[183,1728,386],{"class":188},[183,1730,758],{"class":192},[183,1732,1733,1735,1737,1739,1741,1743,1745,1747,1749],{"class":118,"line":228},[183,1734,773],{"class":188},[183,1736,776],{"class":192},[183,1738,779],{"class":188},[183,1740,782],{"class":188},[183,1742,310],{"class":247},[183,1744,787],{"class":188},[183,1746,790],{"class":192},[183,1748,793],{"class":188},[183,1750,1751],{"class":192}," sv:\n",[183,1753,1754,1757,1759],{"class":118,"line":253},[183,1755,1756],{"class":192},"            costs[fid][sid] ",[183,1758,234],{"class":188},[183,1760,768],{"class":192},[183,1762,1763],{"class":118,"line":272},[183,1764,225],{"emptyLinePlaceholder":224},[183,1766,1767,1770,1772,1775,1777,1780,1783,1786,1788,1790,1792],{"class":118,"line":291},[183,1768,1769],{"class":192},"capacity ",[183,1771,234],{"class":188},[183,1773,1774],{"class":192}," {f[",[183,1776,374],{"class":240},[183,1778,1779],{"class":192},"]: f[",[183,1781,1782],{"class":240},"\"capacity\"",[183,1784,1785],{"class":192},"] ",[183,1787,380],{"class":188},[183,1789,383],{"class":192},[183,1791,386],{"class":188},[183,1793,1794],{"class":192}," stations.getFeatures()}\n",[183,1796,1797,1800,1804],{"class":118,"line":296},[183,1798,1799],{"class":188},"def",[183,1801,1803],{"class":1802},"svObZ"," regret",[183,1805,1806],{"class":192},"(fid):\n",[183,1808,1809,1812,1814,1816],{"class":118,"line":330},[183,1810,1811],{"class":192},"    c ",[183,1813,234],{"class":188},[183,1815,1498],{"class":247},[183,1817,1818],{"class":192},"(costs[fid].values())\n",[183,1820,1821,1824,1827,1829,1831,1833,1836,1838,1841,1843,1845,1848,1851,1854,1857],{"class":118,"line":336},[183,1822,1823],{"class":188},"    return",[183,1825,1826],{"class":192}," (c[",[183,1828,310],{"class":247},[183,1830,1785],{"class":192},[183,1832,307],{"class":188},[183,1834,1835],{"class":192}," c[",[183,1837,46],{"class":247},[183,1839,1840],{"class":192},"]) ",[183,1842,856],{"class":188},[183,1844,496],{"class":247},[183,1846,1847],{"class":192},"(c) ",[183,1849,1850],{"class":188},">",[183,1852,1853],{"class":247}," 1",[183,1855,1856],{"class":188}," else",[183,1858,1859],{"class":192}," math.inf\n",[183,1861,1862,1865,1867],{"class":118,"line":358},[183,1863,1864],{"class":192},"assigned ",[183,1866,234],{"class":188},[183,1868,1172],{"class":192},[183,1870,1871,1873,1876,1878,1880,1883,1887,1889,1892,1895,1897,1899],{"class":118,"line":363},[183,1872,380],{"class":188},[183,1874,1875],{"class":192}," fid ",[183,1877,386],{"class":188},[183,1879,1498],{"class":247},[183,1881,1882],{"class":192},"(costs, ",[183,1884,1886],{"class":1885},"s9osk","key",[183,1888,234],{"class":188},[183,1890,1891],{"class":192},"regret, ",[183,1893,1894],{"class":1885},"reverse",[183,1896,234],{"class":188},[183,1898,347],{"class":247},[183,1900,1901],{"class":192},"):\n",[183,1903,1904,1906,1909,1911,1913,1916,1918,1921,1924,1926],{"class":118,"line":392},[183,1905,750],{"class":188},[183,1907,1908],{"class":192}," sid, c ",[183,1910,386],{"class":188},[183,1912,1498],{"class":247},[183,1914,1915],{"class":192},"(costs[fid].items(), ",[183,1917,1886],{"class":1885},[183,1919,1920],{"class":188},"=lambda",[183,1922,1923],{"class":192}," kv: kv[",[183,1925,310],{"class":247},[183,1927,1928],{"class":192},"]):\n",[183,1930,1931,1933,1936,1938,1940],{"class":118,"line":412},[183,1932,773],{"class":188},[183,1934,1935],{"class":192}," capacity[sid] ",[183,1937,1850],{"class":188},[183,1939,978],{"class":247},[183,1941,981],{"class":192},[183,1943,1944,1947,1949],{"class":118,"line":448},[183,1945,1946],{"class":192},"            assigned[fid] ",[183,1948,234],{"class":188},[183,1950,1951],{"class":192}," sid\n",[183,1953,1954,1957,1960],{"class":118,"line":459},[183,1955,1956],{"class":192},"            capacity[sid] ",[183,1958,1959],{"class":188},"-=",[183,1961,1272],{"class":247},[183,1963,1964],{"class":118,"line":464},[183,1965,1966],{"class":188},"            break\n",[183,1968,1969,1971,1973,1975,1978,1981],{"class":118,"line":488},[183,1970,533],{"class":247},[183,1972,874],{"class":192},[183,1974,544],{"class":247},[183,1976,1977],{"class":192},"(assigned), ",[183,1979,1980],{"class":240},"\"assigned within capacity\"",[183,1982,355],{"class":192},[14,1984,1985,1987,1988,1992],{},[565,1986,567],{}," Keeping the full cost row per location — one cost per facility — allows second choices. Ordering by regret places first the locations that would suffer most from not getting their nearest facility, a classic heuristic that gives sensible results quickly. It is not guaranteed optimal; for formal planning, export the cost matrix to an optimisation library. The ",[21,1989,1991],{"href":1990},"\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Fbuild-origin-destination-matrix-pyqgis\u002F","origin–destination matrix recipe"," builds the same matrix in table form.",[136,1994,1996],{"id":1995},"qgis-version-compatibility","QGIS version compatibility",[14,1998,1999,313,2002,2005,2006,2009,2010,2013,2014,2017,2018,154],{},[180,2000,2001],{},"QgsVectorLayerDirector",[180,2003,2004],{},"QgsGraphBuilder"," and ",[180,2007,2008],{},"QgsGraphAnalyzer.dijkstra"," are available in ",[180,2011,2012],{},"qgis.analysis"," on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. On QGIS 4, ",[180,2015,2016],{},"QgsVectorLayerDirector.DirectionBoth"," is ",[180,2019,2020],{},"QgsVectorLayerDirector.Direction.DirectionBoth",[136,2022,2024],{"id":2023},"troubleshooting","Troubleshooting",[141,2026,2027,2033,2039,2045],{},[144,2028,2029,2032],{},[565,2030,2031],{},"Many addresses are unreachable."," The network has disconnected pieces; check connectivity and snap undershoots.",[144,2034,2035,2038],{},[565,2036,2037],{},"Every address goes to one station."," Station vertices were mixed up with address vertices; check the order of tied points.",[144,2040,2041,2044],{},[565,2042,2043],{},"Runs are slow."," Too many tie points; snap addresses to existing vertices instead of tying each in.",[144,2046,2047,2050],{},[565,2048,2049],{},"Distances look too short."," Cost is in map units of a geographic CRS; reproject to metres.",[136,2052,2054],{"id":2053},"conclusion","Conclusion",[14,2056,2057],{},"Tie facilities and locations into one graph, run one Dijkstra tree per facility and keep each location's minimum cost, write the station and distance back in one call, compare with straight-line assignment to reveal barriers, and use a regret-ordered greedy pass when facilities have limited capacity.",[136,2059,2061],{"id":2060},"frequently-asked-questions","Frequently Asked Questions",[14,2063,2064,2067],{},[565,2065,2066],{},"Can I use travel time instead of distance?","\nYes, with a speed strategy on the director; costs then come out in seconds or hours.",[14,2069,2070,2073],{},[565,2071,2072],{},"What about one-way streets?","\nSet the director's direction field and values so the graph respects one-way restrictions.",[14,2075,2076,2079,2082],{},[565,2077,2078],{},"Is there a Processing algorithm for this?",[180,2080,2081],{},"native:shortestpathpointtolayer"," and service-area algorithms cover related cases; a script is clearer for many-to-many assignment.",[14,2084,2085,2088],{},[565,2086,2087],{},"How many facilities can this handle?","\nEach facility is one Dijkstra run; hundreds are fine.",[136,2090,2092],{"id":2091},"related","Related",[141,2094,2095,2100,2105,2110,2115],{},[144,2096,2097,2099],{},[21,2098,24],{"href":23}," — the guide this recipe belongs to",[144,2101,2102],{},[21,2103,2104],{"href":163},"Build a Network Graph with QgsGraphBuilder",[144,2106,2107],{},[21,2108,2109],{"href":1990},"Build an Origin-Destination Matrix in PyQGIS",[144,2111,2112],{},[21,2113,2114],{"href":1123},"Calculate Service Areas in PyQGIS",[144,2116,2117],{},[21,2118,2120],{"href":2119},"\u002Fspatial-data-processing-automation\u002Fspatial-statistics-and-pattern-analysis\u002Fcreate-voronoi-and-delaunay-pyqgis\u002F","Create Voronoi Polygons and Delaunay Triangles in PyQGIS",[2122,2123,2124],"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 .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}",{"title":178,"searchDepth":202,"depth":202,"links":2126},[2127,2128,2129,2130,2131,2132,2133,2134,2135,2136,2137,2138],{"id":138,"depth":202,"text":139},{"id":167,"depth":202,"text":168},{"id":580,"depth":202,"text":581},{"id":895,"depth":202,"text":896},{"id":1128,"depth":202,"text":1129},{"id":1328,"depth":202,"text":1329},{"id":1615,"depth":202,"text":1616},{"id":1995,"depth":202,"text":1996},{"id":2023,"depth":202,"text":2024},{"id":2053,"depth":202,"text":2054},{"id":2060,"depth":202,"text":2061},{"id":2091,"depth":202,"text":2092},"Assign every address, incident or customer to its closest facility along the road network — one Dijkstra run per facility with QgsGraphAnalyzer, keeping the best cost per location, comparing with straight-line distance, and handling unreachable points and capacity limits.","md",{"slug":2142,"type":2143,"breadcrumb":2144,"datePublished":2145,"dateModified":2145},"find-nearest-facility-pyqgis","article","Find the Nearest Facility","2026-10-02","\u002Fspatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Ffind-nearest-facility-pyqgis",{"title":5,"description":2139},"spatial-data-processing-automation\u002Fnetwork-analysis-and-routing\u002Ffind-nearest-facility-pyqgis\u002Findex","eco9ng7oDsArqmLMLmB862OYlp6-V14tbe6GZoyojnE",1790966263439]