GeoPandas Fundamentals covers matching points to the polygon they fall inside. That question does not always have an answer — a point can easily sit outside every polygon in the other layer — and sometimes it is not even the right question. "Which store is closest to this customer" or "which sensor is nearest this road segment" asks for a nearest match, not a containment test.
Turning coordinates into geometry#
Data rarely starts as geometry. More often it is two plain float columns —
lon and lat — that need to become points before any spatial operation can
touch them:
import geopandas as gpd
points = gpd.GeoDataFrame( df, geometry=gpd.points_from_xy(df.lon, df.lat), crs="EPSG:4326")points_from_xy
builds one Point per row from the two coordinate columns in a single
vectorized call — no loop building Point objects one row at a time.
Wrapping the result in GeoDataFrame
attaches that geometry plus a declared
CRS, here EPSG:4326: plain latitude/longitude, the natural format for
coordinates read out of a GPS device or a location field.
Distance only means something in a projected CRS#
EPSG:4326 is a geographic CRS — coordinates are angles in degrees, not
lengths. A "distance" computed directly in it is a difference in degrees,
which does not correspond to a fixed distance in meters anywhere on Earth. As
covered in GeoPandas Fundamentals,
any distance calculation needs a projected CRS first — reproject with
to_crs before joining, not after.
The nearest-neighbor join#
nearest = gpd.sjoin_nearest( points, lines[["segment_id", "geometry"]], how="left", distance_col="distance_m")sjoin_nearest
asks a different question than sjoin: not "does a predicate hold," but
"which feature in the right-hand layer is closest?" — for every geometry in
the left-hand layer. distance_col writes the computed distance into a new
column, in whatever linear unit the active CRS uses.
distance_m is a confidence signal
A join always returns a nearest feature, even when the true nearest one is hundreds of meters away — a point genuinely outside the other layer's coverage, for instance. Store the distance and treat a large value as a hint to double-check the match, not as an equally trustworthy result.
Handling ties#
A nearest join can return more than one equally-near match — two lines meeting exactly at a point, for example. If the target schema wants exactly one row per point, break the tie explicitly:
nearest = ( nearest.sort_values("distance_m") .drop_duplicates(subset="point_id", keep="first"))Sorting before dropping duplicates guarantees the surviving row is the closest match, not an arbitrary one from the join's own output order.
Common mistakes#
Computing distance in a geographic CRS#
A distance_m computed while still in EPSG:4326 looks like a small,
plausible number and is easy to mistake for meters when it is really
degrees. Reproject to a projected CRS before joining, every time.
Trusting every match equally#
sjoin_nearest never fails to find something. Read distance_col
alongside the match, and decide a sane threshold beyond which a match is not
worth using.
Skipping the tie-break after a nearest join#
Without sort_values + drop_duplicates, a point that ties between two
features can appear twice in the result — silently doubling that point in
anything joined against it downstream.
Further reading#
geopandas.sjoin_nearest— full parameter reference, includingmax_distanceto cap matches.geopandas.points_from_xy— building geometry from coordinate columns.- Nearest neighbor search
— the general problem
sjoin_nearestsolves. - EPSG:4326 — the geographic CRS most raw latitude/longitude data starts in.