Free Maps for Developers: Draw Pins with OpenFreeMap, Compute with MapLark
Draw pins on a free map with OpenFreeMap and MapLibre, then get the data behind them from an OpenStreetMap API. Five real questions: building footprints, flood zones, rooftop solar, shadow studies and cafes.
Try it now
A free map API is easy to find. OpenFreeMap serves vector tiles with no API key, no account, and no usage limit to track, so a pannable, zoomable map costs nothing. The hard part comes next: deciding what to put on it. A basemap draws roads and labels. It does not know which buildings sit in a flood zone or which cafes are wheelchair accessible.
This post covers both halves. First, how to draw pins on a free map. Then, how to get the data behind the pins from an OpenStreetMap API, with five real questions developers search for.
The free map stack
- Tiles: OpenFreeMap. Pick a style URL:
https://tiles.openfreemap.org/styles/bright,positron,dark, orfiord. - Rendering: MapLibre GL JS, the open source fork of Mapbox GL. It reads OpenFreeMap styles directly.
- Data: OpenStreetMap, the same data underneath both OpenFreeMap and MapLark.
Draw pins on a map with OpenFreeMap
A few lines of setup give you a map, and one more adds a pin with a popup. This is the code below running live, with a pin on Chicago. Click the pin.
import maplibregl from 'maplibre-gl';
const map = new maplibregl.Map({
container: 'map',
style: 'https://tiles.openfreemap.org/styles/positron', // no key, no signup
center: [-87.6298, 41.8781], // Chicago
zoom: 12,
});
// One pin with a popup
new maplibregl.Marker()
.setLngLat([-87.6298, 41.8781])
.setPopup(new maplibregl.Popup().setText('Chicago'))
.addTo(map); A Marker is a DOM element, which is fine for a handful of pins. For hundreds or thousands, add one GeoJSON source and one circle layer instead. It renders on the GPU and stays smooth.
// Hundreds of pins: one GeoJSON source and one circle layer, not hundreds of Markers
map.on('load', () => {
map.addSource('pins', { type: 'geojson', data: featureCollection });
map.addLayer({
id: 'pins',
type: 'circle',
source: 'pins',
paint: { 'circle-radius': 5, 'circle-color': '#e4572e' },
});
});That works if you already have the points: store locations, photos, GPS tracks. If you do not, you need a source for them.
What a basemap cannot do
Vector tiles are built for drawing. They are simplified per zoom level, they carry only the tags needed for styling, and you cannot ask them a question such as "give me every building taller than 20 m". The data you want to compute on is full geometry with all its OSM tags. The vector tiles versus OSM features post goes deeper on the difference.
MapLark is the other half. You send a filter, it runs against a planet-scale PostGIS database, and you get back the matching features as GeoJSON, CSV, FlatGeobuf, or GeoParquet. Drop the GeoJSON on the circle layer above and the pins are the answer to your question.
Five questions MapLark answers
Each example below is a real request against the OSM Features API. Numbers come from benchmarks we ran against the production database, not from a toy dataset.
How do I download building footprints for a city for free?
Ask for the polygon table with a building tag and a bounding box. You get one polygon per building, with every tag OSM holds for it.
from osmfeatures import OSMFeaturesClient
with OSMFeaturesClient(api_key="sk-...") as client:
fc = client.query(
bbox="18.05,59.32,18.10,59.34", # central Stockholm
type="way",
way_shape="polygon",
tags=["building"],
)
print(len(fc["features"]))curl -G "https://api.maplark.com/v3/osm_features" \
-H "Authorization: Bearer <your-api-key>" \
-d "bbox=18.05,59.32,18.10,59.34" \
-d "type=way" \
-d "way_shape=polygon" \
--data-urlencode "tags=building" This Stockholm box, 18.05,59.32,18.10,59.34, returns 2,680 buildings with tags=building. Narrowed to tags=building=yes, it returned 532 buildings in 744 to 826 ms in our August comparison, against 1.4 s to a timeout on the public Overpass server. At larger scale the same filter returned 413,984 Manhattan buildings in 32.6 s in a 0.05 deg² benchmark. That size needs a paid tier: the free plan caps a request at 75,000 features and 0.04 deg² for tagged queries. The free plan includes 30,000 credits a month with no card, which covers many city-sized downloads.
How do I find buildings inside a flood zone?
Flood hazard polygons come from your local authority, such as FEMA's National Flood Hazard Layer in the US or the flood maps your national mapping agency publishes in Europe. OpenStreetMap does not hold them, and the within parameter only accepts an OSM way or relation, so the approach is two steps. Fetch every building in the flood polygon's bounding box, then keep the footprints that actually intersect it.
import geopandas as gpd
from osmfeatures import OSMFeaturesClient
# 1. Flood hazard polygons from your authority (FEMA NFHL, an EU flood map, a city GIS portal)
flood = gpd.read_file("flood_hazard_zone.geojson").to_crs(4326)
minx, miny, maxx, maxy = flood.total_bounds
# 2. Every building in the zone's bounding box, with its tags
with OSMFeaturesClient(api_key="sk-...") as client:
fc = client.query(
bbox=f"{minx},{miny},{maxx},{maxy}",
type="way",
way_shape="polygon",
tags=["building"],
)
# 3. Keep the footprints that actually intersect the flood polygon
buildings = gpd.GeoDataFrame.from_features(fc["features"], crs=4326)
at_risk = buildings[buildings.intersects(flood.union_all())]
at_risk.to_file("buildings_in_flood_zone.geojson", driver="GeoJSON") The result is the list of at-risk buildings with their building type, building:levels and addr:* tags where mapped. Draw it on OpenFreeMap with the circle layer, or switch the layer type to fill to shade the footprints. If the zone's bounding box is larger than your plan's area cap, split it into tiles. Using the bbox_tiles or auto_split options in the Python SDK does that for you.
How do I screen rooftops for solar?
Roof size is the first filter. The min_area_m2 parameter drops small buildings on the server, and centroid=True adds a centroid point and area_m2 to each feature, ready to rank or pin. In the Stockholm box above, 1,877 buildings have at least 200 m² of footprint. Excluding building=house and building=garage drops the smallest residential types.
from osmfeatures import OSMFeaturesClient
with OSMFeaturesClient(api_key="sk-...") as client:
# Buildings with at least 200 m2 of footprint. centroid=True adds a centroid point and area_m2 to each feature
fc = client.query(
bbox="18.05,59.32,18.10,59.34",
type="way",
way_shape="polygon",
tags=["building", "!building=house", "!building=garage"],
min_area_m2=200,
centroid=True,
)
# Flat or gabled roofs are the easiest to fit panels on
flat = client.query(
bbox="18.05,59.32,18.10,59.34",
type="way",
way_shape="polygon",
tags=["building", "roof:shape=flat|roof:shape=gabled"],
min_area_m2=200,
) Be honest about what this is. OSM roof tags such as roof:shape are mapped on only part of the buildings in most cities. In that same box only 89 of the large buildings are tagged flat or gabled, so a missing tag does not mean a bad roof. Treat the output as a shortlist to send to a proper survey or an aerial imagery tool, not as a design.
How do I run a shadow study or find tall buildings near a site?
A shadow study needs the heights of the buildings around your site. Search by radius, filter with a numeric comparison, and export the footprints. Start with building:levels, because mappers record it far more often than height.
from osmfeatures import OSMFeaturesClient
with OSMFeaturesClient(api_key="sk-...") as client:
# building:levels is mapped far more often than height (roughly 3 m per level)
tall_by_levels = client.query(
location="59.3293,18.0686",
radius=300,
type="way",
way_shape="polygon",
tags=["building", "building:levels>6"],
)
# Where a mapper recorded an exact height in metres, use it
tall = client.query(
location="59.3293,18.0686",
radius=300,
type="way",
way_shape="polygon",
tags=["building", "height>20"],
) Export as GeoJSON or GeoParquet and load it into QGIS, a three.js scene, or any extrusion tool, using the height value as the extrusion height. Coverage varies a lot between cities. Within 300 m of central Stockholm, 51 of 61 buildings have building:levels, 9 have more than 6 levels, and none has a height tag, so the height>20 query returns nothing there. Count how many of your buildings carry each tag before you rely on it. The Urban Sun Finder case study shows a finished app built on this kind of building data.
How do I get all coffee shops in a city as GeoJSON?
This is the simplest query and the one most people start with. Ask for amenity=cafe around a point and you get a GeoJSON FeatureCollection back, ready for the map.
curl -G "https://api.maplark.com/v3/osm_features" \
-H "Authorization: Bearer <your-api-key>" \
-d "location=59.3293,18.0686" \
-d "radius=1500" \
-d "type=node" \
--data-urlencode "tags=amenity=cafe" \
-o stockholm-cafes.geojsonfrom osmfeatures import OSMFeaturesClient
with OSMFeaturesClient(api_key="sk-...") as client:
cafes = client.query(
location="59.3293,18.0686",
radius=1500,
type="node",
tags=["amenity=cafe"],
)
# cafes is a GeoJSON FeatureCollection: feed it to the circle layer aboveCafes within 1.5 km of central Stockholm came back as 231 elements in 541 to 688 ms in our comparison. The public Overpass server needed 1,038 ms on its best run and timed out on the other two. In a dense benchmark, a 0.005 deg² box over Manhattan returned 1,087 cafes in 213 ms. The full numbers, including the Overpass runs, are in the OSM Features API launch post.
OpenFreeMap or MapLark: which do you need?
| OpenFreeMap | MapLark | |
|---|---|---|
| What you get | A styled background map | Features: geometry and all OSM tags |
| Output | Vector tiles | GeoJSON, CSV, FlatGeobuf, GeoParquet |
| API key | None | Free key, no card |
| Cost | Free | Free tier of 30,000 credits a month, then paid tiers |
| Question it answers | "What does this area look like?" | "Which buildings or places match this filter?" |
When to use which
- OpenFreeMap alone when you already have the points (store locations, photos, GPS tracks) and only need a background map and markers.
- MapLark alone when the output is data, not a picture: a CSV for a spreadsheet, GeoParquet for a notebook, a count for a report, a shortlist for a script.
- Both together when you do not have the points yet and want to see them: flood-zone buildings, tall buildings near a site, cafes in a city. MapLark finds and filters the features and OpenFreeMap draws them.
- Neither when you need live traffic, satellite imagery, street-level photos, or authoritative parcel data. OSM does not cover those, so use a source that does.
Put your first query on a free map
Run a filter in the live demo, then grab a free API key and pipe the result onto OpenFreeMap.