Self-hosted mapping stack. One command per country.
Replace Google Maps API - no API keys, no rate limits, no vendor lock-in.
Routing via Valhalla 3.8.3, geocoding via Photon 1.2.1, vector basemap tiles via Martin 1.13.0 (built with Planetiler 0.10.2).
docker run -p 8002:8002 -p 2322:2322 -p 3000:3000 ghcr.io/roma8ok/getmapstack/cyprusImages are multi-arch: linux/amd64 and linux/arm64 (Apple Silicon, AWS Graviton).
Once it starts, the map catalog UI is at http://localhost:3000/ - browse the vector
tileset, style, fonts and sprites straight from a browser.
Give it a moment to start - Photon opens its search index in a few seconds for a country this size, several minutes for the largest ones. Then check that it answers, a car route from Nicosia to Limassol:
curl localhost:8002/route \
-d '{"locations":[{"lat":35.1856,"lon":33.3823},{"lat":34.6786,"lon":33.0413}],"costing":"auto"}'{"trip":{"summary":{"length":84.736,"time":3770.219,"has_highway":true}}}Every other method is in What you get.
Try the stack without installing - the same services for all supported countries at https://api.getmapstack.com (/valhalla, /photon and /martin prefixes):
curl https://api.getmapstack.com/valhalla/route \
-d '{"locations":[{"lat":35.18,"lon":33.38},{"lat":34.67,"lon":33.04}],"costing":"auto"}'curl "https://api.getmapstack.com/photon/api?q=Nicosia&limit=1"curl "https://api.getmapstack.com/martin/basemap"curl "https://api.getmapstack.com/martin/style/bright/static/33.3823,35.1856,13/600x400.png" -o map.pngPlease keep your usage fair. No SLA - this is a demo that may change or disappear; run your own container for unlimited use.
| Country | Size | Run | |
|---|---|---|---|
| 🇧🇪 | Belgium | 2.3 GB | docker run -p 8002:8002 -p 2322:2322 -p 3000:3000 ghcr.io/roma8ok/getmapstack/belgium |
| 🇧🇳 | Brunei | 0.8 GB | docker run -p 8002:8002 -p 2322:2322 -p 3000:3000 ghcr.io/roma8ok/getmapstack/brunei |
| 🇨🇾 | Cyprus | 0.4 GB | docker run -p 8002:8002 -p 2322:2322 -p 3000:3000 ghcr.io/roma8ok/getmapstack/cyprus |
| 🇮🇩 | Indonesia | 2.7 GB | docker run -p 8002:8002 -p 2322:2322 -p 3000:3000 ghcr.io/roma8ok/getmapstack/indonesia |
| 🇰🇿 | Kazakhstan | 1.4 GB | docker run -p 8002:8002 -p 2322:2322 -p 3000:3000 ghcr.io/roma8ok/getmapstack/kazakhstan |
| 🇲🇾 | Malaysia | 1.1 GB | docker run -p 8002:8002 -p 2322:2322 -p 3000:3000 ghcr.io/roma8ok/getmapstack/malaysia |
| 🇸🇬 | Singapore | 0.9 GB | docker run -p 8002:8002 -p 2322:2322 -p 3000:3000 ghcr.io/roma8ok/getmapstack/singapore |
| 🇰🇷 | South Korea | 1.9 GB | docker run -p 8002:8002 -p 2322:2322 -p 3000:3000 ghcr.io/roma8ok/getmapstack/south-korea |
| 🇻🇳 | Vietnam | 1.2 GB | docker run -p 8002:8002 -p 2322:2322 -p 3000:3000 ghcr.io/roma8ok/getmapstack/vietnam |
All three engines serve their full API, not a trimmed subset - see Not included
for the handful of features these images don't have the data to answer. Everything below
runs against a plain docker run of a country image, with no configuration.
| Task | Request | Service |
|---|---|---|
| Route between two or more points | POST /route |
/valhalla (:8002) |
| Route for a given departure or arrival time | POST /route |
/valhalla (:8002) |
| Visit many stops in the best order | POST /optimized_route |
/valhalla (:8002) |
| Time and distance for many pairs at once | POST /sources_to_targets |
/valhalla (:8002) |
| Area reachable within N minutes | POST /isochrone |
/valhalla (:8002) |
| Meeting point for several starting points | POST /centroid |
/valhalla (:8002) |
| Snap a GPS track to the road network | POST /trace_route |
/valhalla (:8002) |
| Per-segment attributes of a snapped track | POST /trace_attributes |
/valhalla (:8002) |
| Inspect what the router explored | POST /expansion |
/valhalla (:8002) |
| Nearest road to a coordinate | POST /locate |
/valhalla (:8002) |
| Road network as vector tiles | POST /tile |
/valhalla (:8002) |
| Coordinates from a place name, search and autocomplete | GET /api?q= |
/photon (:2322) |
| Coordinates from address fields | GET /structured |
/photon (:2322) |
| Address from coordinates | GET /reverse |
/photon (:2322) |
| Vector basemap tiles for any map library | GET /basemap/{z}/{x}/{y} |
/martin (:3000) |
| Map style, fonts, icons | GET /style/bright, /font/..., /sprite/... |
/martin (:3000) |
| Static map images, no JS | GET/POST /style/bright/static/... |
/martin (:3000) |
| Rendered raster tiles | GET /style/bright/{z}/{x}/{y}.png |
/martin (:3000) |
| Live PostGIS overlay | MARTIN_POSTGRES env |
/martin (:3000) |
| Versions and how old the data is | GET /status |
/valhalla, /photon |
| Liveness check for the map service | GET /health |
/martin (:3000) |
Self-hosted, call the port directly: localhost:8002/route. On the hosted API the same
paths sit behind a prefix: https://api.getmapstack.com/valhalla/route.
Parameter-level reference for all three: Valhalla API · Photon API · Martin docs
Cost options shape the result. This one avoids highways and tolls, asks for two alternatives and kilometers:
curl localhost:8002/route -d '{
"locations":[{"lat":35.1856,"lon":33.3823},{"lat":34.6786,"lon":33.0413}],
"costing":"auto",
"costing_options":{"auto":{"use_highways":0.2,"use_tolls":0}},
"units":"kilometers",
"alternates":2
}'{"trip":{"summary":{"length":90.545,"time":5700.343,"has_highway":false}},"alternates":[...]}The default car route between the same points is 84.7 km in 63 minutes over the highway; avoiding it costs 5.8 km and 32 minutes.
Costing profiles: auto, bicycle, pedestrian, truck, motorcycle, bus, taxi,
motor_scooter. Truck costing takes vehicle dimensions:
curl localhost:8002/route -d '{
"locations":[{"lat":35.1856,"lon":33.3823},{"lat":34.6786,"lon":33.0413}],
"costing":"truck",
"costing_options":{"truck":{"height":4.11,"weight":21.77,"axle_load":9.07}}
}'To route around an area, pass exclude_polygons - all polygons in one request may total
at most 10 km of perimeter, summed across every polygon rather than measured per polygon.
Each vertex is a [lon, lat] pair, the reverse of the lat/lon keys locations uses
in the same request:
curl localhost:8002/route -d '{
"locations":[{"lat":35.1856,"lon":33.3823},{"lat":34.6786,"lon":33.0413}],
"costing":"auto",
"exclude_polygons":[[[33.36,35.15],[33.38,35.15],[33.38,35.17],[33.36,35.17],[33.36,35.15]]]
}'{"trip":{"summary":{"length":88.282,"time":4004.178}}}Decoding the shape: every leg carries shape as an encoded polyline at precision
6, while Google's algorithm and most off-the-shelf decoders default to precision 5.
Decoded at precision 5, the route lands roughly ten times away from where it belongs.
Use a precision-6 decoder, or ask for GeoJSON instead:
curl localhost:8002/route -d '{
"locations":[{"lat":35.1856,"lon":33.3823},{"lat":34.6786,"lon":33.0413}],
"costing":"auto",
"shape_format":"geojson"
}'date_time accepts type: 0 for "depart now", type: 1 for "depart at", type: 2
for "arrive by", and type: 3 for "invariant" - the clock does not advance along the
route, so every road is evaluated at the same moment. The answer carries local times
and time zones per location:
curl localhost:8002/route -d '{
"locations":[{"lat":35.1856,"lon":33.3823},{"lat":34.6786,"lon":33.0413}],
"costing":"auto",
"date_time":{"type":1,"value":"2026-08-03T08:00"}
}'{"trip":{"locations":[
{"lat":35.1856,"lon":33.3823,"date_time":"2026-08-03T08:00","time_zone_offset":"+03:00","time_zone_name":"Asia/Nicosia"},
{"lat":34.6786,"lon":33.0413,"date_time":"2026-08-03T09:01","time_zone_offset":"+03:00","time_zone_name":"Asia/Nicosia"}]}}Departure time picks up time-of-day access restrictions and daylight-saving arithmetic, including routes that cross a time zone. It does not change travel speeds: the images carry no traffic data, so travel time is the same at rush hour and at night.
Reorders the stops between the first and the last to make the trip shortest. Nicosia, Paphos, Larnaca, Limassol in that order is 350.4 km:
curl localhost:8002/optimized_route -d '{
"locations":[{"lat":35.1856,"lon":33.3823},{"lat":34.7754,"lon":32.4245},
{"lat":34.9229,"lon":33.6233},{"lat":34.6786,"lon":33.0413}],
"costing":"auto"
}'{"trip":{"summary":{"length":249.218},"locations":[{"original_index":0},{"original_index":2},{"original_index":1},{"original_index":3}]}}original_index maps each stop back to its position in the request - here the optimizer
saved 101 km.
Travel time and distance for every source-target pair, in one request:
curl localhost:8002/sources_to_targets -d '{
"sources":[{"lat":35.1856,"lon":33.3823}],
"targets":[{"lat":34.9229,"lon":33.6233},{"lat":35.0333,"lon":33.2000}],
"costing":"auto"
}'{"sources_to_targets":[[{"from_index":0,"to_index":0,"time":2259,"distance":47.447},
{"from_index":0,"to_index":1,"time":3975,"distance":32.935}]]}Keep the pairs regional: distant pairs come back with null time and distance even
though a direct route request between the same points succeeds.
How far you get in 10 and 20 minutes by car, as polygons:
curl localhost:8002/isochrone -d '{
"locations":[{"lat":35.1856,"lon":33.3823}],
"costing":"auto",
"contours":[{"time":10,"color":"ff0000"},{"time":20,"color":"0000ff"}],
"polygons":true,
"denoise":0.5,
"generalize":50
}'{"type":"FeatureCollection","features":[{"properties":{"contour":20.0,"metric":"time","color":"#0000ff"},"geometry":{"type":"Polygon","coordinates":[...]}}]}Drop the coordinates straight into any map library. Contours can be distances instead of
times ({"distance":1.5}). Limits: up to 4 contours, 120 minutes or 200 km per contour,
one location per request.
Where several people should meet, by travel time rather than by geometry:
curl localhost:8002/centroid -d '{
"locations":[{"lat":35.1856,"lon":33.3823},{"lat":34.6786,"lon":33.0413},{"lat":34.9229,"lon":33.6233}],
"costing":"auto"
}'{"trip":{"locations":[{"lat":35.1856,"lon":33.3823},{"lat":34.83793,"lon":33.40012}],"summary":{"length":42.461,"time":929.531}}}With three input stops, the top-level trip only carries the route from the first stop
(trip.locations[0]) to the converging point (trip.locations[1]). The routes from the
other input stops to that same point come back as full route objects - each with its own
trip.locations and summary - in the alternates array, one per remaining stop in
input order.
Snap a raw GPS trace onto the road network and get a normal route back:
curl localhost:8002/trace_route -d '{
"shape":[{"lat":35.1856,"lon":33.3823},{"lat":35.1860,"lon":33.3830},
{"lat":35.1869,"lon":33.3841},{"lat":35.1880,"lon":33.3855}],
"costing":"auto",
"shape_match":"map_snap"
}'{"trip":{"summary":{"length":0.584,"time":79.172},"legs":[{"maneuvers":[{"street_names":["Agiou Dimitriou"]}]}]}}/trace_attributes returns the matched road segments instead of driving instructions,
and filters keeps the response to the attributes you asked for:
curl localhost:8002/trace_attributes -d '{
"shape":[{"lat":35.1856,"lon":33.3823},{"lat":35.1860,"lon":33.3830},
{"lat":35.1869,"lon":33.3841},{"lat":35.1880,"lon":33.3855}],
"costing":"auto",
"shape_match":"map_snap",
"filters":{"attributes":["edge.names","edge.speed","edge.road_class","edge.length"],"action":"include"}
}'{"edges":[{"names":["Agiou Dimitriou"],"speed":30,"road_class":"residential","length":0.039},
{"names":["Plapouta"],"speed":30,"road_class":"residential","length":0.007}]}The search tree the router walked, as GeoJSON - useful for debugging a surprising route or visualizing reachability:
curl localhost:8002/expansion -d '{
"locations":[{"lat":35.1856,"lon":33.3823}],
"costing":"auto",
"action":"isochrone",
"contours":[{"time":1}],
"expansion_properties":["edge_id","cost","distance","edge_status"]
}'{"type":"FeatureCollection","properties":{"algorithm":"dijkstras"},
"features":[{"properties":{"distance":161,"cost":21,"edge_status":"s","edge_id":3473392795306}}]}action can also be route; expect a large response, since it contains every edge the
search touched.
What road a coordinate belongs to:
curl localhost:8002/locate -d '{
"locations":[{"lat":35.1856,"lon":33.3823}],
"costing":"auto",
"verbose":true
}'[{"input_lat":35.1856,"input_lon":33.3823,"edges":[{"edge_info":{"names":["Zappeiou"],"way_id":21056849,"speed_limit":0}}]}]speed_limit is 0 where OSM carries no maxspeed tag - routing still uses the profile's
default speed for that road class.
The routing graph itself, as Mapbox Vector Tiles - layers edges, nodes, shortcuts
and access_restrictions, with per-edge attributes like road class and speed. Not a
basemap: no buildings, land use or labels, just every road the router knows about. The
tile address goes in a nested tile object; the common /z/x/y.mvt path form is not
supported:
curl localhost:8002/tile -d '{"tile":{"z":14,"x":9711,"y":6479}}' -o nicosia.mvtThe same request as a GET with URL-encoded JSON works as a tile template - point MapLibre at it and the road network renders like any vector source:
sources: {
valhalla: {
type: "vector",
tiles: ["http://localhost:8002/tile?json=%7B%22tile%22%3A%7B%22z%22%3A{z}%2C%22x%22%3A{x}%2C%22y%22%3A{y}%7D%7D"],
minzoom: 7
}
}Style the edges source-layer to see the network. Low zoom levels carry only the bigger
road classes; the endpoint is marked beta upstream. For a real basemap with buildings,
land use, water and labels, see Map.
Place name to coordinates. lang picks the language of the returned names:
curl "localhost:2322/api?q=Nicosia&limit=1&lang=en"{"features":[{"geometry":{"type":"Point","coordinates":[33.3638783,35.1746503]},
"properties":{"name":"Nicosia","type":"district","country":"Cyprus","state":"Cyprus"}}]}Without lang the same query returns local names - Λευκωσία - Lefkoşa, Κύπρος - Kıbrıs.
The index is built for prefix matching, so autocomplete is the same endpoint with a partial query:
curl "localhost:2322/api?q=Limas&limit=5&lang=en"{"features":[{"properties":{"name":"Limassol","type":"district"}},
{"properties":{"name":"Limassol District","type":"county"}},
{"properties":{"name":"Limassol","type":"city"}},
{"properties":{"name":"Limassol Medieval Castle","type":"house"}},
{"properties":{"name":"Limassol Salt Lake","type":"other"}}]}Narrow the results by proximity, by bounding box, by result layer, by country, or by
OSM tag - bbox takes minLon,minLat,maxLon,maxLat, the reverse order of the
lat/lon query parameters on the same endpoint:
curl "localhost:2322/api?q=Agios&limit=3&lat=34.6786&lon=33.0413&lang=en"
curl "localhost:2322/api?q=Agios&limit=3&bbox=32.9,34.6,33.2,34.8&lang=en"
curl "localhost:2322/api?q=Larnaca&limit=3&layer=city&lang=en"
curl "localhost:2322/api?q=Nicosia&limit=3&countrycode=CY&lang=en"
curl "localhost:2322/api?q=hospital&limit=3&osm_tag=amenity:hospital&lang=en"The last one turns the geocoder into a POI search:
{"features":[{"properties":{"name":"Nicosia General Hospital","osm_value":"hospital","city":"Apostolos Varnavas & Agios Makarios"}},
{"properties":{"name":"Paphos General Hospital","osm_value":"hospital","city":"Paphos"}},
{"properties":{"name":"Lito Private Hospital","osm_value":"hospital","city":"Paralimni"}}]}A few more dials: zoom and location_bias_scale tune how strongly the lat/lon
bias pulls results toward the focus point, dedupe=0 keeps near-duplicate entries the
geocoder would otherwise fold, and include/exclude filter by category
(osm.<key>.<value>). A category with no q at all is pure discovery - everything of
one kind near a point:
curl "localhost:2322/api?include=osm.amenity.hospital&limit=3&lat=35.1856&lon=33.3823&lang=en"When the address already comes split into fields - a checkout form, a CRM record - skip
free-text guessing and pass the fields directly. Any subset of street, housenumber,
city, district, county, state, postcode and countrycode works:
curl "localhost:2322/structured?street=Zappeiou&housenumber=21&city=Nicosia&lang=en"{"features":[{"geometry":{"type":"Point","coordinates":[33.3824628,35.1852708]},
"properties":{"housenumber":"21","street":"Zappeiou","city":"Nicosia","postcode":"1036","type":"house"}}]}At least one field is required, and q is not accepted here - free text and structured
fields cannot mix in one request.
Coordinates to address:
curl "localhost:2322/reverse?lat=35.1853&lon=33.3825&limit=1"{"features":[{"properties":{"street":"Zappeiou","housenumber":"21","city":"Λευκωσία","postcode":"1036"}}]}radius (km) widens the search, layer restricts what comes back - streets only, for
example:
curl "localhost:2322/reverse?lat=35.1853&lon=33.3825&radius=5&limit=3&layer=street&lang=en"{"features":[{"properties":{"name":"Perikleous","type":"street"}},
{"properties":{"name":"Zappeiou","type":"street"}},
{"properties":{"name":"Gianni Tsiatala","type":"street"}}]}Buildings come back without a name - they carry housenumber and street instead, so
check the number of features rather than the presence of a name.
Martin serves a full vector basemap built by Planetiler from the same OSM snapshot as
routing and geocoding - tiles, fonts, icons, a ready-to-use style, static map images and
rendered raster tiles, all on port 3000. Any map rendered from these tiles must show
visible credit: (c) OpenMapTiles (c) OpenStreetMap contributors. The tileset carries that
string in its TileJSON, so a MapLibre map built on the shipped style displays it on its own.
Static images and rendered raster tiles come back as bare pixels - whatever page or
document they land in has to carry the credit.
The shipped style is OpenFreeMap Bright,
a maintained fork of OSM Bright, vendored here with five edits and the icons that come with
it; make update-bright-style diffs the vendored copy against upstream. The tiles follow
the OpenMapTiles schema, so any style written for that schema works instead - point a client
at your own and serve it from wherever you like.
TileJSON describes the tileset; /basemap/{z}/{x}/{y} serves the tiles themselves:
curl localhost:3000/basemap{"tiles":["http://localhost:3000/basemap/{z}/{x}/{y}"],"name":"OpenMapTiles","attribution":"<a href=\"https://www.openmaptiles.org/\" target=\"_blank\">© OpenMapTiles</a> <a href=\"https://www.openstreetmap.org/copyright\" target=\"_blank\">© OpenStreetMap contributors</a>","bounds":[31.95244,34.2337399,34.96147,36.00323],"minzoom":0,"maxzoom":14}Point MapLibre GL JS at the ready-made style instead of wiring up sources by hand:
import maplibregl from "maplibre-gl";
new maplibregl.Map({
container: "map",
style: "http://localhost:3000/style/bright",
center: [33.3823, 35.1856],
zoom: 12
});/font/{fontstack}/{range} cuts glyph PBFs on the fly for whatever text a style's layers
need - curl "localhost:3000/font/Noto%20Sans%20Regular/0-255" (URL-encode the space in
the fontstack name). /sprite/bright.png and
/sprite/bright.json (plus @2x and an sdf_sprite/ variant for tintable icons) serve
the icon images the style's point layers reference.
Render a PNG or JPEG server-side, no browser or JavaScript involved - point, zoom and size go in the path:
curl "localhost:3000/style/bright/static/33.3823,35.1856,13/600x400.png" -o map.pngPOST a GeoJSON FeatureCollection to draw markers, lines or polygons on top of the same
view:
curl -X POST "localhost:3000/style/bright/static/33.3823,35.1856,13/600x400.png" \
-H "Content-Type: application/json" \
-d '{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[33.3823,35.1856]},"properties":{"circle-radius":8,"circle-color":"#e11"}}]}' \
-o map-overlay.pngBoth come back as a 600x400 PNG - the second one with a red dot over Nicosia.
The same style pre-rendered into ordinary {z}/{x}/{y}.png raster tiles, for clients
that don't speak vector tiles:
curl "localhost:3000/style/bright/12/2427/1619.png" -o tile.pngThe style's tile/glyph/sprite URLs default to http://localhost:3000 - fine inside a
single container, wrong once a browser on another machine needs to fetch them. Set
PUBLIC_URL and the entrypoint templates it into the style at startup:
docker run -p 8002:8002 -p 2322:2322 -p 3000:3000 \
-e PUBLIC_URL=https://maps.example.com \
ghcr.io/roma8ok/getmapstack/cyprusStatic images and rendered raster tiles are fetched by Martin from inside the container,
so if PUBLIC_URL is not reachable from there, render through the second style
(/style/bright-local/static/..., /style/bright-local/{z}/{x}/{y}.png) instead of
/style/bright/... - it always points at localhost:3000 no matter what PUBLIC_URL is
set to. bright is the one to hand to browsers.
Set MARTIN_POSTGRES to a Postgres connection string and Martin adds every table with a
geometry column as an extra tile source at /{table}/{z}/{x}/{y}, auto-discovered, no
config file to edit:
environment:
MARTIN_POSTGRES: postgres://user:password@host:5432/dbnameLeave it unset and the map stays fully static - no database, no extra moving part.
The routing and geocoding engines report their version and when their data was built:
curl localhost:8002/status{"version":"3.8.3","tileset_last_modified":1785100041,"available_actions":[...]}curl localhost:2322/status{"status":"Ok","import_date":"2026-07-25T18:00:19Z","version":"1.2.1","git_commit":"b9d6ab92"}Both timestamps are build times, not the OpenStreetMap snapshot date. Every published
image also carries a date tag matching the OSM extract it was built from, next to
latest. The vector tileset carries the same snapshot: all three services build from one
pinned OSM download.
The map service answers curl localhost:3000/health with OK - a liveness check, no
version or build date. The tileset's own metadata sits in its
TileJSON instead.
- Elevation. Tiles are built without elevation data:
/heightanswers withnullvalues and routes carry no grade. Adding it means changing how the tiles are built and supplying a separate elevation dataset - it is not a flag on these images. - Traffic. No live or historical traffic. Travel times use free-flow speeds, so a route takes the same time at 08:00 and at 23:00.
- Public transport. No GTFS feed is imported.
costing: multimodalfails with "Locations are in unconnected regions" rather than falling back to walking, and/transit_availablealways answersfalse. - Full geometries. The geocoding index stores points only:
geometry=1answers HTTP 400. Area features still carry anextentbounding box where OSM has one. - One country per image. A route that leaves the country in the image has no data to follow - run the image for the country you need, or run several.
Requires Docker. Build a country image locally instead of pulling from GHCR:
git clone https://github.com/roma8ok/getmapstack.git
cd getmapstack
make build-valhalla-builder
make build-photon-builder
make build-planetiler-builder
make fetch-osm COUNTRY=cyprus
make create-valhalla-tiles COUNTRY=cyprus
make create-photon-data COUNTRY=cyprus
make create-vector-tiles COUNTRY=cyprus
make build-server COUNTRY=cyprus
docker run -p 8002:8002 -p 2322:2322 -p 3000:3000 getmapstack/cyprusfetch-osm pins one OSM snapshot so all three builders work from the same download. Intermediate artifacts (routing tiles, geocoding index, vector tiles) land in artifacts/. Images build for linux/amd64 and linux/arm64 by default - pass PLATFORMS=linux/arm64 (or your platform) for a faster single-arch build. make help lists all targets and available countries.
Code: MIT. Map data: © OpenStreetMap contributors, ODbL 1.0, sourced from Geofabrik extracts.
The images embed OSM-derived databases (routing tiles, geocoding index, vector tiles) redistributed under ODbL 1.0 - see NOTICE for full attribution. If you publicly use routing or geocoding results from these images, credit OpenStreetMap: "© OpenStreetMap contributors" linked to openstreetmap.org/copyright.
The basemap adds third-party design work: the tiles follow the OpenMapTiles schema (CC-BY 4.0), the style is OpenFreeMap Bright - a fork of OpenMapTiles' OSM Bright, style code BSD-3-Clause, style design CC-BY 4.0, the fork's own changes MIT - with Maki icons under CC0 1.0 and Noto fonts under OFL 1.1. Full license texts ship inside every image at /usr/share/doc/getmapstack/THIRD_PARTY_LICENSES.