Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Getmapstack

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).

One docker run command starts a container with Valhalla routing on port 8002, Photon geocoding on port 2322 and Martin vector tiles on port 3000, backed by OSM data baked into the image; your application talks to all three.

Quick start

docker run -p 8002:8002 -p 2322:2322 -p 3000:3000 ghcr.io/roma8ok/getmapstack/cyprus

Images 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.

Hosted API

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.png

Please keep your usage fair. No SLA - this is a demo that may change or disappear; run your own container for unlimited use.

Countries

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

What you get

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

Routing

Route

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"
}'

Time-dependent route

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.

Optimized route

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.

Time and distance matrix

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.

Isochrone

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.

Meeting point

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.

Map matching

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}]}

Expansion

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.

Locate

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.

Road network tiles

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.mvt

The 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.

Geocoding

Search

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"

Structured search

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.

Reverse geocoding

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.

Map

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.

Vector tiles

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\">&copy; OpenMapTiles</a> <a href=\"https://www.openstreetmap.org/copyright\" target=\"_blank\">&copy; 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
});

Fonts and icons

/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.

Static images

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.png

POST 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.png

Both come back as a 600x400 PNG - the second one with a red dot over Nicosia.

Rendered raster tiles

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.png

Serving to another host

The 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/cyprus

Static 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.

Live PostGIS overlay

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/dbname

Leave it unset and the map stays fully static - no database, no extra moving part.

Data freshness

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.

Not included

  • Elevation. Tiles are built without elevation data: /height answers with null values 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: multimodal fails with "Locations are in unconnected regions" rather than falling back to walking, and /transit_available always answers false.
  • Full geometries. The geocoding index stores points only: geometry=1 answers HTTP 400. Area features still carry an extent bounding 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.

Build it yourself

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/cyprus

fetch-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.

License

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.

About

Self-hosted mapping stack. One command per country. Replace Google Maps API - no API keys, no rate limits, no vendor lock-in.

Resources

Stars

Watchers

Forks

Packages

Contributors

Languages