Introduction
relying on Shapely for geometric operations. It provides an intuitive framework to manipulate and analyze geographic data directly within Python, making it possible to work seamlessly with formats such as shapefiles, GeoJSON, and even spatial databases.
A common challenge in geospatial analysis is determining the geographic context of a location. In practice, this often translates into a simple but fundamental question: given a pair of latitude and longitude coordinates, which country does this point belong to?
In this guide, we will walk through a complete and practical workflow to answer this question. We will start by loading country boundary data from Natural Earth, then show how to query individual points efficiently. Along the way, we will highlight common pitfalls, such as duplicate matches arising from multi-part geometries, and demonstrate how to address them. Finally, we will compare a straightforward approach with a more optimized, vectorized solution that scales well to large datasets.
Download Natural Earth dataset
We will use the Natural Earth 110m resolution dataset, which provides global country boundaries.
Natural Earth Data is a public domain dataset that provides geographical data at various scales, including country boundaries, roads, cities, and more. Natural Earth Data can be downloaded for free from their website.
Download here:
https://www.naturalearthdata.com/downloads/110m-cultural-vectors/
Download the file:
1 | ne_110m_admin_0_countries.shp |
Unzip it and place it in your working directory.
Install GeoPandas
Before we dive into retrieving country names, let's first make sure that we have geopandas installed. To install geopandas, you can use pip for example:
1 | pip install geopandas |
Load the dataset
Let's utilize geopandas to explore the Natural Earth 110m cultural dataset:
1 2 3 4 5 6 7 8 9 10 | import geopandas as gpd from shapely.geometry import Point # Load shapefile gdf = gpd.read_file("110m_cultural/ne_110m_admin_0_countries.shp") # Keep only relevant columns gdf = gdf[['NAME', 'geometry']] print(gdf.head()) |

Important:
Natural Earth data is already in geographic coordinates (EPSG:4326), which matches latitude/longitude.
Retrieve Country Names from Latitude/Longitude
Define a geographic point
To retrieve the country name for a specific point, we first need to define our point using its latitude and longitude values. Let's say we want to find out which country is located at the following coordinates.
In geopandas, we can use the shapely Point class to create our point:
1 2 3 4 | target_lon = 2.2137 target_lat = 46.227 point = Point(target_lon, target_lat) |
Method 1 — Basic approach (loop + contains)
1 2 3 4 5 6 7 8 9 10 | found_country = None for _, row in gdf.iterrows(): geom = row.geometry if geom.contains(point): found_country = row['NAME'] break print(found_country) |
Output:
1 | France
|
Note: Why duplicates can occur (e.g. "France" twice). Example with
1 2 3 4 5 6 7 8 9 | for index, row in gdf.iterrows(): if 'MultiPolygon' in row['geometry'].geom_type: for polygon in row['geometry'].geoms: if polygon.contains(point): print( row['NAME'] ) if 'Polygon' in row['geometry'].geom_type: polygon = row['geometry'] if polygon.contains(point): print( row['NAME'] ) |
Countries like France are stored as MultiPolygon geometries:
- mainland France
- overseas territories (e.g., French Guiana, islands)
If you loop through each sub-geometry manually, you may match multiple polygons → duplicate outputs.
Method 2 — Recommended (vectorized spatial join)
This is the best and most scalable approach.
1 2 3 4 5 6 7 8 9 10 11 12 13 | # Create GeoDataFrame for the point point_gdf = gpd.GeoDataFrame( geometry=[Point(target_lon, target_lat)], crs="EPSG:4326" ) # Ensure CRS consistency gdf = gdf.to_crs(point_gdf.crs) # Spatial join result = gpd.sjoin(point_gdf, gdf, predicate="within") print(result['NAME'].iloc[0]) |
Output:
1 | France
|
Why this method is better
- No loops (vectorized)
- Uses spatial indexing (R-tree)
- Avoids duplicate results
- Much faster for large datasets
This is especially important if you:
- process large datasets
- run batch geolocation pipelines
- integrate with H3 grids
Handle edge cases (border points)
Sometimes a point lies exactly on a boundary, and contains() returns False.
Use instead:
1 | result = gpd.sjoin(point_gdf, gdf, predicate="intersects") |
Method 3 — Multiple points at once
1 2 3 4 5 6 7 8 9 10 11 | points = gpd.GeoDataFrame( geometry=[ Point(2.21, 46.22), # France Point(-95.71, 37.09), # USA ], crs="EPSG:4326" ) result = gpd.sjoin(points, gdf, predicate="within") print(result[['geometry', 'NAME']]) |
Output:
1 2 3 | geometry NAME 0 POINT (2.21 46.22) France 1 POINT (-95.71 37.09) United States of America |
Visualization (optional but recommended)
1 2 3 4 5 6 7 8 | import folium m = folium.Map(location=[46.2, 2.2], zoom_start=5) folium.GeoJson(gdf).add_to(m) folium.Marker([target_lat, target_lon]).add_to(m) m |

This is very useful for:
- debugging
- validating spatial joins
- quick QA checks
Summary
| Method | Pros | Cons |
|---|---|---|
| Loop + contains | Simple | Slow |
| Loop + break | Fixes duplicates | Still slow |
Spatial join (sjoin) |
Fast, scalable, clean | Requires GeoDataFrame |
Other methods
See also your related article:
https://en.moonbooks.org/Articles/How-to-retrieve-country-name-for-a-given-latitude-and-longitude-using-python-/
References
| Link | Description |
|---|---|
| https://www.naturalearthdata.com/downloads/110m-cultural-vectors/ | Natural Earth dataset |
| https://geopandas.org/en/stable/docs/reference/api/geopandas.sjoin.html | GeoPandas spatial join |
| https://shapely.readthedocs.io/en/stable/manual.html | Shapely geometry operations |
| https://proj.org | Coordinate reference systems |
| https://rtree.readthedocs.io | Spatial indexing backend |
| https://en.wikipedia.org/wiki/Point_in_polygon | Point-in-polygon concept |
Final tip (advanced users)
For high-performance pipelines (like with VIIRS / H3):
Consider:
- pre-building spatial indexes
- using vectorized joins on batches
- combining with H3 indexing for coarse filtering
This can improve performance by orders of magnitude on large-scale datasets.
