6k views and no answer with code example. I've worked exactly on this problem so I thought I should bring this back from the dead to help you next person who stumbles upon this;
Marco13's answer was the way to go; using a Quadtree.
(My work is based off of https://github.com/Josephbakulikira/QuadTree-Visualization-with-python--pygame)
I implemented a Quadtree in python, so I could check streets closest to specific addresses on a map database. I think this represents pretty much what the OP wanted to do.
My quadtree takes in lines segments, and points. Which are in my case road segments, and house locations.
I can then query the quadtree at a specific point with a certain radius, to pick road segments close to this point, and get the closest segment.
Here is the code:
quadtree.py:
from shapes import Rectangle, Vector2, Line, Circle class QuadTree: """ Python representation of a QuadTree """ def __init__(self, capacity : int, boundary : Rectangle) -> None: self.capacity : int = capacity self.boundary : Rectangle = boundary self.elements = set([]) self.northWest : QuadTree = None self.northEast : QuadTree = None self.southWest : QuadTree = None self.southEast : QuadTree = None return @property def children(self) -> list | bool: """ Checker if tree has children. If yes, returns them in list """ if self.northWest != None: return [self.northWest, self.northEast, self.southWest, self.southEast] else: return False def subdivide(self) -> None: """ Divide current tree into quadrants (children) """ parent = self.boundary differencex = (parent.bottomRight.x - parent.topLeft.x) / 2 differencey = (parent.bottomRight.y - parent.topLeft.y) / 2 # Setup children boundaries boundary_nw = Rectangle( Vector2(parent.topLeft.x , parent.topLeft.y), Vector2(parent.bottomRight.x - differencex, parent.bottomRight.y - differencey) ) boundary_ne = Rectangle( Vector2(parent.topLeft.x + differencex, parent.topLeft.y), Vector2(parent.bottomRight.x, parent.bottomRight.y - differencey) ) boundary_sw = Rectangle( Vector2(parent.topLeft.x, parent.topLeft.y + differencey), Vector2(parent.topLeft.x + differencex, parent.bottomRight.y) ) boundary_se = Rectangle( Vector2(parent.topLeft.x + differencex, parent.topLeft.y + differencey), Vector2(parent.bottomRight.x, parent.bottomRight.y) ) self.northWest = QuadTree(self.capacity, boundary_nw) self.northEast = QuadTree(self.capacity, boundary_ne) self.southWest = QuadTree(self.capacity, boundary_sw) self.southEast = QuadTree(self.capacity, boundary_se) # Add parent's elements to children for element in self.elements: self.insertinchildren(element) # Clear elements from parent once they are added to children self.elements = set([]) return def insert(self, element) -> bool: """ Insert element into tree. If tree is at max capacity, subdivide it, and add elements to children """ if self.boundary.containsElement(element) == False: return False if len(self.elements) < self.capacity and not self.children: self.elements.add(element) return True else: if len(self.elements) == self.capacity and not self.children: if type(element) == Line: if element in self.elements: return True if not self.children: self.subdivide() return self.insertinchildren(element) def insertinchildren(self, element) -> bool: """ Insert element into children (Insert lines everywhere they intersect, while inserting points(Vectors) only in one quadrant they are in) """ if isinstance(element, Line): self.northWest.insert(element) self.northEast.insert(element) self.southWest.insert(element) self.southEast.insert(element) return True else: if not self.northWest.insert(element): if not self.northEast.insert(element): if not self.southWest.insert(element): if not self.southEast.insert(element): return False return True def query(self, _range : Rectangle | Circle) -> set: """ Use a circle or rectangle to query the tree for elements in range """ elements_in_range = set([]) if _range.intersects(self.boundary): if self.children: elements_in_range.update(self.northWest.query(_range)) elements_in_range.update(self.northEast.query(_range)) elements_in_range.update(self.southWest.query(_range)) elements_in_range.update(self.southEast.query(_range)) else: for element in self.elements: if _range.containsElement(element): elements_in_range.add(element) return elements_in_range
shapes.py
import numpy as np class Vector2(np.ndarray): """ Numpy array subclass to represent a 2D vector """ def __new__(cls, x, y) -> np.ndarray: return np.array([x, y]).view(cls) @property def x(self) -> float: return self[0] @property def y(self) -> float: return self[1] def __hash__(self) -> int: return id(self) def __eq__(self, other) -> bool: if not isinstance(other, Vector2): return False return np.array_equal(self, other) class Line: """ Representation of a line segment """ def __init__(self, point1 : Vector2, point2 : Vector2) -> None: self.a = point1 self.b = point2 return def __str__(self) -> str: return f"Line({self.a}, {self.b})" def distancetoPoint(self, p : Vector2) -> float: """ Get the shortest distance between point p and the line segment """ # Distance between the closest point and the point p return np.linalg.norm(self.closestPoint(p) - p) def closestPoint(self, p : Vector2) -> float: """ Get the closest point on the line segment from point p """ a = self.a b = self.b cp = None # Closest point ab = b - a ap = p - a proj = np.dot(ap, ab) abLenSq = ab[0]**2 + ab[1]**2 normalizedProj = proj / abLenSq if normalizedProj <= 0: cp = a elif normalizedProj >= 1: cp = b else: cp = a + normalizedProj * ab return cp def onSegment(self, p, q, r) -> bool: """ Given three collinear points p, q, r, the function checks if point q lies on line segment 'pr' """ if ( (q.x <= max(p.x, r.x)) and (q.x >= min(p.x, r.x)) and (q.y <= max(p.y, r.y)) and (q.y >= min(p.y, r.y))): return True return False def orientation(self, p, q, r) -> int: """ To find the orientation of an ordered triplet (p,q,r) function returns the following values: 0 : Collinear points 1 : Clockwise points 2 : Counterclockwise See https://www.geeksforgeeks.org/orientation-3-ordered-points/amp/ for details of below formula. """ val = (float(q.y - p.y) * (r.x - q.x)) - (float(q.x - p.x) * (r.y - q.y)) if (val > 0): # Clockwise orientation return 1 elif (val < 0): # Counterclockwise orientation return 2 else: # Collinear orientation return 0 def intersects(self, p1 : Vector2, p2 : Vector2) -> bool: """ Check if current line intersects with another line made of point1 and point2 """ # Find the 4 orientations required for # the general and special cases o1 = self.orientation(self.a, self.b, p1) o2 = self.orientation(self.a, self.b, p2) o3 = self.orientation(p1, p2, self.a) o4 = self.orientation(p1, p2, self.b) # General case if ((o1 != o2) and (o3 != o4)): return True # _____________ Special Cases _____________ # self.a , self.b and p1 are collinear and p1 lies on segment p1q1 if ((o1 == 0) and self.onSegment(self.a, p1, self.b)): return True # self.a , self.b and p2 are collinear and p2 lies on segment p1q1 if ((o2 == 0) and self.onSegment(self.a, p2, self.b)): return True # p1 , p2 and self.a are collinear and self.a lies on segment p2q2 if ((o3 == 0) and self.onSegment(p1, self.a, p2)): return True # p1 , p2 and self.b are collinear and self.b lies on segment p2q2 if ((o4 == 0) and self.onSegment(p1, self.b, p2)): return True # If none of the cases return False class Rectangle: """ Rectangle used as boundaries for the QuadTree or as a query range for the QuadTree """ def __init__(self, topLeft : Vector2, bottomRight : Vector2) -> None: self.topLeft = topLeft self.bottomRight = bottomRight self.topRight = Vector2(self.bottomRight.x, self.topLeft.y) self.bottomLeft = Vector2(self.topLeft.x, self.bottomRight.y) self.center = (self.topLeft + self.bottomRight) / 2 self.top = Line(self.topLeft, Vector2(self.bottomRight.x, self.topLeft.y)) self.bottom = Line(Vector2(self.topLeft.x, self.bottomRight.y), self.bottomRight) self.left = Line(self.topLeft, Vector2(self.topLeft.x, self.bottomRight.y)) self.right = Line(Vector2(self.bottomRight.x, self.topLeft.y), self.bottomRight) return def __str__(self) -> str: return f"Rectangle({self.topLeft}, {self.bottomRight})" def containsElement(self, element : Vector2 | Line) -> bool: """ Checks if the element is contained by this rectangle """ if isinstance(element, Line): return self.containsLine(element) x, y = element bx , by = self.topLeft cx, cy = self.bottomRight if x >= bx and x <= cx and y >= by and y <= cy: return True else: return False def containsLine(self, line : Line) -> bool: """ Checks if the line goes through this rectangle """ # Check if any end of the line is inside the rectangle if self.containsElement(line.a) or self.containsElement(line.b): return True # Check if line intersects with any side of the rectangle else: if not line.intersects(self.top.a, self.top.b): if not line.intersects(self.bottom.a, self.bottom.b): if not line.intersects(self.left.a, self.left.b): if not line.intersects(self.right.a, self.right.b): return False return True def intersects(self, other : 'Rectangle') -> bool: """ Check if this rectangle intersects with another rectangle ie. if any of the sides of the rectangle crosses another rectangle """ return not (self.bottomRight.x < other.topLeft.x or self.topLeft.x > other.bottomRight.x or self.bottomRight.y < other.topLeft.y or self.topLeft.y > other.bottomRight.y) class Circle: """ Circle used as range to query the QuadTree """ def __init__(self, center : Vector2, radius : float) -> None: self.center = center self.radius = radius return def __str__(self) -> str: return f"Circle({self.center}, r{self.radius})" def containsElement(self, element : Vector2 | Line) -> bool: """ Checks if the element is contained by this circle """ if isinstance(element, Line): return self.containsLine(element) dist = np.linalg.norm(self.center - element) if dist <= self.radius: return True else: return False def containsLine(self, line : Line) -> bool: """ Checks if the line goes through this circle """ if line.distancetoPoint(self.center) <= self.radius: return True else: return False def intersects(self, rectangle : Rectangle) -> bool: """ Check if this circle intersects with a rectangle """ # There are only two cases where a circle and a rectangle can intersect: # 1. The circle center is inside the rectangle if not rectangle.containsElement(self.center): # 2. The circle intersects with one of the rectangle's sides if not self.containsLine(rectangle.top): if not self.containsLine(rectangle.bottom): if not self.containsLine(rectangle.left): if not self.containsLine(rectangle.right): return False return True
main.py
from quadtree import QuadTree from shapes import Rectangle, Circle, Vector2, Line # Setup lines with Overpass nodes node0 = Vector2(107.1360616, 46.1032011) node1 = Vector2(107.13563750000003, 46.102939) node2 = Vector2(107.13538800000003, 46.1028447) node3 = Vector2(107.13493060000002, 46.1027272) node4 = Vector2(107.13318809999998, 46.1021657) node5 = Vector2(107.13255700000002, 46.1019075) node6 = Vector2(107.1316817, 46.1014684) node7 = Vector2(107.13088449999998, 46.1011467) node8 = Vector2(107.1304002, 46.1010027) node9 = Vector2(107.1299055, 46.1009219) node10 = Vector2(107.12915320000002, 46.1008729) node11 = Vector2(107.12877689999999, 46.1008901) node12 = Vector2(107.12828999999999, 46.10097) node13 = Vector2(107.1274209, 46.1012472) node14 = Vector2(107.1270212, 46.1014094) node15 = Vector2(107.1265209, 46.101732) node16 = Vector2(107.12579249999999, 46.1022694) node17 = Vector2(107.1256219, 46.1024435) node18 = Vector2(107.12546250000003, 46.1026809) node19 = Vector2(107.12500690000002, 46.1034946) nodes = [node0, node1, node2, node3, node4, node5, node6, node7, node8, node9, node10, node11, node12, node13, node14, node15, node16, node17, node18, node19] lines = [] for i in range(len(nodes)-1): lines.append(Line(nodes[i], nodes[i+1])) # Setup points house0 = Vector2(107.13595197486893, 46.10285045021604) house1 = Vector2(107.1300706177878, 46.101115083412374) house2 = Vector2(107.13043003379954, 46.100917943095524) house3 = Vector2(107.13046758472615, 46.101234111186926) house4 = Vector2(107.13071434795808, 46.10100721426972) house5 = Vector2(107.13108449280605, 46.10108904605243) house6 = Vector2(107.12480471447458, 46.10391049893959) house7 = Vector2(107.12635081112013, 46.10232085692584) houses = [house0, house1, house2, house3, house4, house5, house6, house7] # Setup quadtree tree = QuadTree(2, Rectangle(Vector2(107.12383, 46.09690), Vector2(107.13709, 46.10700))) for l in lines: tree.insert(l) for h in houses: tree.insert(h) # Setup query queryAT = Vector2(107.12739, 46.10358) radQuery = Circle(queryAT, 0.0022) found_elements = tree.query(radQuery) print("Found elements:") for element in found_elements: print(element) # Find closest line shortest_dist = 999999 closest_line = None for element in found_elements: if isinstance(element, Line): dist = element.distancetoPoint(queryAT) if dist < shortest_dist: shortest_dist = dist closest_line = element print("\nClosest line: " + str(closest_line)) # ---------- Represent everything in a graph ------------ import matplotlib.pyplot as plt import matplotlib.patches as patches fig, ax = plt.subplots() ax.set_xlim(tree.boundary.topLeft.x, tree.boundary.bottomRight.x) ax.set_ylim(tree.boundary.topLeft.y, tree.boundary.bottomRight.y) def plot_tree(ax, quadtree): """ Traverse the quadtree recursively and plot the boundaries and elements of each node """ if quadtree.children: for child in quadtree.children: plot_tree(ax, child) else: ax.add_patch(patches.Rectangle((quadtree.boundary.topLeft.x, quadtree.boundary.topLeft.y), quadtree.boundary.bottomRight.x-quadtree.boundary.topLeft.x, quadtree.boundary.bottomRight.y - quadtree.boundary.topLeft.y, fill=False)) for element in quadtree.elements: clr = 'black' if element in found_elements: clr = 'magenta' if element is closest_line: clr = 'red' if isinstance(element, Line): ax.plot([element.a.x, element.b.x], [element.a.y, element.b.y], color=clr) #plot end segments as dots: ax.plot(element.a.x, element.a.y, 'o', color=clr) ax.plot(element.b.x, element.b.y, 'o', color=clr) elif isinstance(element, Vector2): ax.plot(element.x, element.y, 'go') return plot_tree(ax, tree) # Add query circle to plot ax.add_patch(patches.Circle((queryAT.x, queryAT.y), radQuery.radius, fill=False, color='orange')) # add circle center to plot ax.plot(queryAT.x, queryAT.y, 'o', color='orange') ax.set_box_aspect(1) # make the plot square plt.show()
So in main.py I use coordinates points and setup a Quadtree of an area of approx. 1km square with only one street made of several line segments.
I then query a specific spot (107.12739, 46.10358) lon,lat , at a distance of 0.0022 = around 170meters
This gives me the results: 
Visualization in matplotlib: Orange is the query circle Magenta are the segments found by the query The red line is the closest line


Is it really more efficient for 1000 segments? I don't know. Anyway, there you go.