tiny-osm · Exploring OpenStreetMap data¶
tiny_osm.fetch is the single entry point. It takes a WGS84 bounding box
and an osm_filter that controls which Overpass QL tag filters are sent:
osm_filter |
What it queries |
|---|---|
OSMFilters.HIGHWAY |
All highway=* ways, excluding areas, abandoned / planned / raceway tags |
OSMFilters.WATERWAY |
All waterway=* ways |
OSMFilters.WATER_BODY |
Closed water features: ponds, lakes, reservoirs, detention/retention basins |
| any string | Raw Overpass QL tag-filter (e.g. '["amenity"="restaurant"]') |
The return value is a GeoJSON FeatureCollection dict — standards-compliant
and directly consumable by any geospatial library. Each feature carries the
OSM tags in its properties alongside osm_type and osm_id, so you can
filter and style without extra bookkeeping.
This notebook fetches a ~5 km² slice of downtown Austin, TX, then builds several maps to show what you can do with the GeoJSON output.
1 · Fetch layers¶
from __future__ import annotations
import geopandas as gpd
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import tiny_osm
from tiny_osm import OSMFilters
# ~5 km x 5 km box centred on downtown Austin, TX
BBOX = (-97.775, 30.245, -97.725, 30.295)
highways_fc = tiny_osm.fetch(*BBOX, osm_filter=OSMFilters.HIGHWAY)
waterways_fc = tiny_osm.fetch(*BBOX, osm_filter=OSMFilters.WATERWAY)
water_bodies_fc = tiny_osm.fetch(*BBOX, osm_filter=OSMFilters.WATER_BODY)
2 · Load layers into GeoPandas¶
Because every layer is already a valid GeoJSON FeatureCollection, loading
it is a single GeoDataFrame.from_features call, no element-to-geometry
bookkeeping, no node-ID lookups, no polygon assembly. Tags land as columns,
ready to filter on.
streets = gpd.GeoDataFrame.from_features(highways_fc, crs=4326)
rivers = gpd.GeoDataFrame.from_features(waterways_fc, crs=4326)
water_bodies = gpd.GeoDataFrame.from_features(water_bodies_fc, crs=4326)
3 · Filter streets by highway tag¶
Every feature returned by OSMFilters.HIGHWAY carries its OSM tags as
columns on the GeoDataFrame. The highway tag is the primary classifier:
| Value | Meaning |
|---|---|
motorway, trunk, primary |
Arterial / major roads |
secondary, tertiary |
Collector streets |
residential, living_street |
Neighbourhood roads |
footway, path, steps, pedestrian |
Pedestrian-only infrastructure |
cycleway |
Dedicated bike lanes |
service |
Driveways, parking aisles, alleys |
ROAD_TYPES = {
"motorway",
"trunk",
"primary",
"secondary",
"tertiary",
"motorway_link",
"trunk_link",
"primary_link",
"secondary_link",
"tertiary_link",
}
RESIDENTIAL = {"residential", "living_street", "unclassified", "road"}
PEDESTRIAN = {"footway", "path", "steps", "pedestrian", "corridor", "track"}
CYCLING = {"cycleway"}
SERVICE = {"service"}
hw = streets["highway"]
roads_gdf = streets[hw.isin(ROAD_TYPES)]
residential_gdf = streets[hw.isin(RESIDENTIAL)]
pedestrian_gdf = streets[hw.isin(PEDESTRIAN)]
cycling_gdf = streets[hw.isin(CYCLING)]
service_gdf = streets[hw.isin(SERVICE)]
4 · Map, street network by type¶
fig, ax = plt.subplots(figsize=(11, 11), facecolor="#1a1a2e")
ax.set_facecolor("#1a1a2e")
kw = {"ax": ax}
service_gdf.plot(color="#3a3a5c", linewidth=0.5, **kw)
residential_gdf.plot(color="#4a6fa5", linewidth=0.8, **kw)
pedestrian_gdf.plot(color="#e8a838", linewidth=0.7, alpha=0.85, **kw)
cycling_gdf.plot(color="#5dd35d", linewidth=1.2, **kw)
roads_gdf.plot(color="#f0f0f0", linewidth=1.6, **kw)
rivers.plot(color="#4fc3f7", linewidth=1.4, **kw)
if not water_bodies.empty:
water_bodies.plot(color="#1565c0", alpha=0.55, **kw)
legend_handles = [
mpatches.Patch(color="#f0f0f0", label="Major roads"),
mpatches.Patch(color="#4a6fa5", label="Residential"),
mpatches.Patch(color="#e8a838", label="Pedestrian"),
mpatches.Patch(color="#5dd35d", label="Cycling"),
mpatches.Patch(color="#3a3a5c", label="Service / driveways"),
mpatches.Patch(color="#4fc3f7", label="Waterways"),
mpatches.Patch(color="#1565c0", label="Water bodies"),
]
ax.legend(
handles=legend_handles,
loc="lower right",
framealpha=0.25,
facecolor="#1a1a2e",
edgecolor="none",
labelcolor="white",
fontsize=9,
)
ax.set_title("Austin, TX, street network by type", color="white", fontsize=14, pad=12)
ax.set_axis_off()
plt.tight_layout()
fig.savefig("images/austin_tx.svg", bbox_inches="tight", transparent=True)
plt.show()
5 · Waterway detail¶
Rivers and named waterways coloured by waterway tag type.
if "name" not in rivers.columns:
rivers["name"] = None
# Waterways come back as a mix of LineStrings (rivers/streams) and Polygons
# (riverbanks, docks). Restrict to linear features for the stream/river plots.
line_mask = rivers.geometry.geom_type == "LineString"
rivers_lines = rivers[line_mask]
rivers_only = rivers_lines[rivers_lines["waterway"] == "river"]
streams = rivers_lines[rivers_lines["waterway"].isin(["stream", "canal", "drain"])]
fig, ax = plt.subplots(figsize=(11, 9), facecolor="#0d1b2a")
ax.set_facecolor("#0d1b2a")
streets.plot(color="#1e2d3d", linewidth=0.4, ax=ax, alpha=0.6)
streams.plot(color="#81d4fa", linewidth=0.9, ax=ax, alpha=0.8)
rivers_only.plot(color="#4fc3f7", linewidth=2.2, ax=ax)
if not water_bodies.empty:
water_bodies.plot(color="#1565c0", alpha=0.6, ax=ax, edgecolor="#4fc3f7", linewidth=0.5)
# Label named rivers at the midpoint of each way
for _, row in rivers_only[rivers_only["name"].notna()].iterrows():
mid = row.geometry.interpolate(0.5, normalized=True)
ax.text(
mid.x,
mid.y,
row["name"],
fontsize=7,
color="#b3e5fc",
ha="center",
bbox={"boxstyle": "round,pad=0.2", "fc": "#0d1b2a", "alpha": 0.5, "ec": "none"},
)
legend_handles = [
mpatches.Patch(color="#4fc3f7", label="River"),
mpatches.Patch(color="#81d4fa", label="Stream / canal / drain"),
mpatches.Patch(color="#1565c0", label="Water body"),
]
ax.legend(
handles=legend_handles,
loc="lower right",
framealpha=0.25,
facecolor="#0d1b2a",
edgecolor="none",
labelcolor="white",
fontsize=9,
)
ax.set_title("Austin, TX, waterways & water bodies", color="white", fontsize=14, pad=12)
ax.set_axis_off()
plt.tight_layout()
plt.show()
6 · Active-travel network (pedestrians + cyclists)¶
Isolating only human-powered movement infrastructure makes it easy to see how well-connected an area is for walking and cycling.
fig, ax = plt.subplots(figsize=(11, 9), facecolor="#0f2027")
ax.set_facecolor("#0f2027")
streets.plot(color="#1c2e3d", linewidth=0.35, ax=ax, alpha=0.7)
rivers.plot(color="#1a6b8a", linewidth=1.0, ax=ax, alpha=0.7)
pedestrian_gdf.plot(color="#ffb347", linewidth=0.9, ax=ax, alpha=0.9)
cycling_gdf.plot(color="#7fff7f", linewidth=1.5, ax=ax)
legend_handles = [
mpatches.Patch(color="#ffb347", label="Pedestrian paths"),
mpatches.Patch(color="#7fff7f", label="Cycleways"),
mpatches.Patch(color="#1c2e3d", label="Other streets (context)"),
]
ax.legend(
handles=legend_handles,
loc="lower right",
framealpha=0.25,
facecolor="#0f2027",
edgecolor="none",
labelcolor="white",
fontsize=9,
)
ax.set_title("Austin, TX, active travel network", color="white", fontsize=14, pad=12)
ax.set_axis_off()
plt.tight_layout()
plt.show()
7 · Summary¶
n_highways = len(highways_fc["features"])
n_waterways = len(waterways_fc["features"])
n_water_bodies = len(water_bodies_fc["features"])