How to Get Points of Interest From OpenStreetMap in Python
On this page
Problem statement
Restaurants, schools, pharmacies, shops, museums: OpenStreetMap is the largest open database of points of interest, and for many cities it is more complete than any commercial list. Getting them into a clean table takes more than one query, though. A POI can be mapped as a node, as a building outline or as a site polygon. Categories are spread across several keys, amenity, shop, tourism and leisure, and names, opening hours and other details are present for some features and not others.
This page extracts every point of interest in Washington, D.C. from the OpenStreetMap extract of 1 January 2026. It turns nodes and polygons into one point layer, counts the categories, measures how many POIs carry a name and opening hours, and finds the restaurants within walking distance of Metro stations.
Quick answer
Read the POI keys with pyosmium, then reduce every geometry to a single point:
fp = (osmium.FileProcessor(str(PBF)).with_areas()
.with_filter(osmium.filter.KeyFilter("amenity", "shop", "tourism"))
.with_filter(osmium.filter.GeoInterfaceFilter(tags=["amenity", "shop", "tourism", "name", "opening_hours"])))
raw = gpd.GeoDataFrame.from_features(fp, crs=4326)
P = raw[raw.geom_type != "LineString"].copy() # closed ways come back as lines too
P["geometry"] = P.to_crs(32618).representative_point().to_crs(4326)
P = P[P.within(DC)]
P["category"] = np.where(P.amenity.notna(), "amenity", np.where(P.shop.notna(), "shop", "tourism"))
print(f"read {len(raw):,} features; {len(P):,} POIs inside D.C.; {P.category.value_counts().to_dict()}")
read 27,975 features; 20,044 POIs inside D.C.; {'amenity': 16697, 'shop': 2107, 'tourism': 1240}
Of the 27,975 features read, 20,044 points lie inside the District: 16,697 tagged amenity, 2,107 shop and 1,240 tourism.
Common types, what counts as a POI, restaurants near stations
1. The most common types
print("amenity:", P.amenity.value_counts().head(8).to_dict())
print("shop:", P.shop.value_counts().head(6).to_dict())
print("tourism:", P.tourism.value_counts().head(5).to_dict())
amenity: {'parking': 6040, 'bench': 2083, 'restaurant': 1080, 'bicycle_parking': 656, 'fast_food': 590, 'place_of_worship': 584, 'waste_basket': 532, 'parking_space': 503}
shop: {'convenience': 251, 'alcohol': 202, 'hairdresser': 171, 'clothes': 169, 'beauty': 129, 'supermarket': 83}
tourism: {'artwork': 462, 'information': 254, 'hotel': 182, 'attraction': 153, 'museum': 74}
Parking dominates amenity, with 6,040 car parks and 503 individual spaces, followed by 2,083 benches. Restaurants come third, at 1,080. Among shops, convenience stores (251) and liquor stores (202) lead. Among tourism features, 462 artworks outnumber the 182 hotels and 74 museums.
2. Keep what a reader would call a POI
amenity includes benches, waste baskets and parking spaces. Keep the types people travel to:
VISIT = {"restaurant", "cafe", "fast_food", "bar", "pub", "pharmacy", "school", "library", "bank", "place_of_worship", "theatre", "cinema", "hospital", "post_office"}
V = P[P.amenity.isin(VISIT) | P.shop.notna() | P.tourism.isin({"museum", "hotel", "attraction", "gallery"})]
print(f"POIs people visit: {len(V):,} of {len(P):,}")
print(f"with a name: {V.name.notna().mean():.0%}; with opening_hours: {V.opening_hours.notna().mean():.0%}")
POIs people visit: 6,323 of 20,044
with a name: 97%; with opening_hours: 33%
Keeping the places people travel to leaves 6,323 POIs. 97% of them have a name, but only 33% have opening hours.
3. Restaurants near Metro stations
Combine two extracts: restaurants and the Metro stations, railway=station with station=subway:
fp = (osmium.FileProcessor(str(PBF)).with_filter(osmium.filter.TagFilter(("railway", "station")))
.with_filter(osmium.filter.GeoInterfaceFilter(tags=["name", "station"])))
S = gpd.GeoDataFrame.from_features(fp, crs=4326)
S = S[(S.station == "subway") & S.within(DC)].to_crs(32618)
R = V[V.amenity == "restaurant"].to_crs(32618)
near = R.geometry.apply(lambda p: S.distance(p).min() <= 400)
print(f"Metro stations in D.C.: {len(S)}; restaurants: {len(R):,}; within 400 m of a station: {near.sum():,} ({near.mean():.0%})")
Metro stations in D.C.: 44; restaurants: 1,080; within 400 m of a station: 481 (45%)
There are 44 elements tagged as subway stations inside the District. Of the 1,080 restaurants, 481, or 45%, are within 400 m of one of them.
Points of interest within a station's walk
The data used on this page
The Geofabrik OpenStreetMap extract for the District of Columbia dated 1 January 2026 (Β© OpenStreetMap contributors, ODbL), and the District's boundary from the US Census Bureau's 2023 cartographic boundary files at 1:500,000, used to drop features from the extract's margins in Maryland and Virginia.
from pathlib import Path
import geopandas as gpd
import numpy as np
import osmium
import requests
Path("data").mkdir(exist_ok=True)
def cached(path, url):
p = Path(path)
if not p.exists():
r = requests.get(url, timeout=600)
r.raise_for_status()
p.write_bytes(r.content)
return p
PBF = cached("data/district-of-columbia-260101.osm.pbf", "https://download.geofabrik.de/north-america/us/district-of-columbia-260101.osm.pbf")
C = gpd.read_file(cached("data/cb_2023_us_county_500k.zip", "https://www2.census.gov/geo/tiger/GENZ2023/shp/cb_2023_us_county_500k.zip"))
DC = C[C.GEOID == "11001"].to_crs(4326).geometry.iloc[0]
print(f"D.C. boundary: {C[C.GEOID == '11001'].to_crs(32618).area.iloc[0] / 1e6:.1f} kmΒ²")
D.C. boundary: 176.9 kmΒ²
The District's boundary covers 176.9 kmΒ², water included.
Example 1: POIs by type within a station's walk
List what lies within 400 m of one station, Gallery Place-Chinatown:
st = S[S.name.str.contains("Gallery", na=False)].geometry.iloc[0]
around = V.to_crs(32618)[V.to_crs(32618).distance(st) <= 400]
label = around.amenity.fillna(around.shop.fillna(around.tourism))
print(f"within 400 m of {S[S.name.str.contains('Gallery', na=False)].name.iloc[0]}: {len(around)} POIs")
print(label.value_counts().head(6).to_dict())
within 400 m of Gallery Place: 202 POIs
{'restaurant': 59, 'fast_food': 29, 'clothes': 14, 'cafe': 13, 'bar': 12, 'place_of_worship': 11}
Within 400 m of Gallery Place there are 202 POIs, led by 59 restaurants and 29 fast-food places.
A POI is whatever a mapper tagged
A point of interest in OpenStreetMap is whatever a mapper tagged with one of the POI keys: amenity for services and facilities, shop for retail, tourism for hotels, museums and attractions, plus leisure, office and craft for more. The same cafΓ© may be a node in the middle of the street, a node on the building, or a tag on the building polygon itself, depending on who mapped it and when. Reducing every geometry to a representative point inside it, and dropping the line copies that pyosmium returns for closed ways, gives one point per feature.
What makes OSM POIs useful is also what makes them uneven. Coverage depends on local mappers: a downtown restaurant is almost always there, while a dentist on a residential street may not be. Attributes such as opening hours, cuisine or wheelchair access are optional and far less complete than names. Before counting POIs per area, check names and a sample of locations against local knowledge, and keep the extract's date with the results, because the map changes every day.
Several keys, chains and closed places
- Several keys on one feature: a pub with
amenity=pubandtourism=attractionappears once; pick the key that defines your category explicitly. - Chains: the
brandandbrand:wikidatatags identify chain stores consistently across spellings of the name. - Closed or historic places: lifecycle prefixes such as
disused:shopkeep closed places out of the main keys. - Live queries: for small areas, OSMnx's
features_from_placegets the same data from the Overpass API.
Internal links
- How to read an OpenStreetMap PBF file in Python β the file used here
- How to find EV charging stations in OpenStreetMap with Python β one POI type in depth
- Fixing OSM features counted twice β why the line copies are dropped
FAQ
How do I get points of interest from OpenStreetMap?
Filter an OSM extract or the Overpass API for the amenity, shop and tourism keys, and convert polygons to points with representative_point().
Why are some POIs polygons?
Mappers can tag a whole building or site instead of placing a node; reduce such features to a point inside them.
How complete is OpenStreetMap POI data?
It varies by place and type; names are usually present, while opening hours and other details are much sparser.
What is the difference between amenity and shop in OSM?
amenity covers services and facilities such as restaurants, schools and banks; shop covers retail.