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.

Map of 6,323 points of interest inside the D.C. boundary, coloured as food and drink, shops, tourism and other services, densest downtown and along the main avenues. Map of 6,323 points of interest inside the D.C. boundary, coloured as food and drink, shops, tourism and other services, densest downtown and along the main avenues.
Points of interest people visit.

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.

The most common amenity values in Washington, D.C. amenity values inside D.C.parking6,040bench2,083restaurant1,080bicycle_parking656fast_food590place_of_worship5840200040006000Blue: places people travel to spatialworkflow.io
Most amenities are not destinations.

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.

Map of the 202 points of interest within a dashed 400-metre circle around Gallery Place-Chinatown station, food and drink in red and other places in blue. Map of the 202 points of interest within a dashed 400-metre circle around Gallery Place-Chinatown station, food and drink in red and other places in blue.
A station's walking catchment.

A POI is whatever a mapper tagged

Turning OpenStreetMap POIs mapped in different ways into one point layer. one point per POInodea pointbuildingpolygontagged amenity=site polygoncampus, parkdrop linecopiesclosed ways twicerepresentativepointinside each shape20,044 POIs inside D.C. spatialworkflow.io
One point per POI.

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

Checks before using OpenStreetMap points of interest. before trusting POI countsamenity, shop, tourismand leisure, officedrop street furniturebenches, bins, parkingnames97% have oneopening hoursonly 33%record the extract datethe map changes dailyD.C., OSM extract of 1 January 2026 spatialworkflow.io
Before trusting POI counts.
  • Several keys on one feature: a pub with amenity=pub and tourism=attraction appears once; pick the key that defines your category explicitly.
  • Chains: the brand and brand:wikidata tags identify chain stores consistently across spellings of the name.
  • Closed or historic places: lifecycle prefixes such as disused:shop keep closed places out of the main keys.
  • Live queries: for small areas, OSMnx's features_from_place gets the same data from the Overpass API.

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.