Migrate from Google Maps to Maptoolkit
The Google Maps JavaScript API shares no interface with MapLibre GL JS or Leaflet, so the map layer is a rewrite rather than a port. The service APIs are a different story: Directions, Distance Matrix, Geocoding and Elevation all have direct counterparts, and moving those is mechanical.
Plan the work in that order. The services can move first, on their own, while the map stays where it is.
What maps to what
| Google Maps Platform | Maptoolkit | What differs |
|---|---|---|
| Maps JavaScript API | MapLibre GL JS or Leaflet | Different library entirely. See below |
| Maps Static API | Static Maps API | Different parameter names, same idea |
| Directions API | Routing API | Car, bike, foot and transit profiles |
| Distance Matrix API | Matrix API | Travel times and distances between origins and destinations |
| Geocoding API | Geocoding API | Forward and reverse |
| Elevation API | Elevation API | Point lookups |
| Maps SDK for Android and iOS | MapLibre GL Native | Open source, same style URLs as the web |
Maptoolkit also has a Weather API, an Isochrone API and a Map Matching API. Google has no first-party isochrone product at all, so if you have been approximating reachability with repeated Distance Matrix calls, that is worth looking at during the move.
What does not map
- Places API. Autocomplete, place details, photos, opening hours, ratings and reviews have no equivalent. The Geocoding API resolves addresses and place names; it is not a business directory. For many migrations this is the deciding factor, so check it first.
- Street View. No equivalent, and no plan for one.
- Traffic layer and traffic-aware routing. Routes are computed on the road network without
live traffic, so
departure_timehas no congestion model behind it. - Roads API. Snap to Roads has a counterpart in the Map Matching API; speed limits do not.
- Maps Embed API. No equivalent iframe product. Embed a real map instead.
- Routes API route optimization. No equivalent. Stop ordering and vehicle routing are not offered.
- Cloud-based map styling. Style JSON is edited as a file rather than in a console, and custom cartography on a Maptoolkit account is an Enterprise arrangement.
Coordinate order
Google uses {lat: 48.21, lng: 16.37} objects, or LatLng instances, latitude first.
MapLibre and GeoJSON use [16.37, 48.21] arrays, longitude first. Leaflet is the
exception: it takes [48.21, 16.37], latitude first like Google, but GeoJSON data you load into
it is still longitude first.
The two orders are both plausible-looking numbers, so a swapped pair does not throw. It puts your map in the wrong place, usually in the sea off Somalia if the coordinates were European. Wrap the conversion in one function early and use it everywhere:
const toLngLat = (g) => [g.lng, g.lat];The service APIs are their own case: point parameters on the routing endpoints are
lat,lng, latitude first, matching Google rather than the map libraries. Check the reference
for the endpoint you are calling rather than assuming either order.
Rewriting the map
There are two paths, and the service APIs work the same with both:
- MapLibre GL JS, the default. It is open source, draws the Maptoolkit vector styles from a style URL, and supports 3D terrain, rotation and restyling in the browser. The style switcher and isochrone control are available as MapLibre plugins.
- Leaflet, if you want the smallest change of mental model. Its markers, popups and polylines are objects on the map, as in Google, and it takes coordinates latitude first. It draws Maptoolkit raster tiles, so no 3D, no rotation and no restyling in the browser.
With MapLibre GL JS
- <script src="https://maps.googleapis.com/maps/api/js?key=GOOGLE_KEY"></script>
+ <script src="https://cdn.jsdelivr.net/npm/maplibre-gl@5.6.1/dist/maplibre-gl.js"></script>
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@5.6.1/dist/maplibre-gl.css" />
- const map = new google.maps.Map(document.getElementById("map"), {
- center: { lat: 48.21, lng: 16.37 },
- zoom: 12,
- });
+ const map = new maplibregl.Map({
+ container: "map",
+ style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
+ center: [16.37, 48.21],
+ zoom: 12,
+ attributionControl: { compact: false },
+ });
Note what happened to the centre. Google takes an object with named lat and lng, so the
order is unambiguous; here it is a bare array in [longitude, latitude] order. A swapped pair does not raise an
error, see Coordinate order.
The rest of the translation, in the order you will hit it:
| MapLibre GL JS | |
|---|---|
new google.maps.Marker({position, map}) | new maplibregl.Marker().setLngLat(c).addTo(map) |
new google.maps.InfoWindow({content}) | new maplibregl.Popup().setHTML(content) |
map.setCenter(), map.setZoom() | map.setCenter(), map.setZoom(), or map.jumpTo({center, zoom}) |
map.panTo() | map.easeTo({center}) or map.flyTo({center}) |
map.fitBounds(LatLngBounds) | map.fitBounds([[w, s], [e, n]]) |
google.maps.event.addListener(map, "click", fn) | map.on("click", fn) |
e.latLng.lat(), e.latLng.lng() | e.lngLat.lat, e.lngLat.lng |
new google.maps.Polyline({path}) | a GeoJSON source plus a line layer |
new google.maps.Data() | map.addSource(id, {type: "geojson", data}) |
| MarkerClusterer | the cluster options on a GeoJSON source |
styles: [...] on the map options | the style JSON itself |
Two differences are structural rather than cosmetic.
Zoom is fractional. MapLibre zoom is a float, so zoom: 12.5 is valid and the map
animates continuously between levels. Google’s integer levels map onto it directly, but layout
tuned to integer steps may need revisiting.
Vector data goes into the style, not onto the map. Google attaches overlay objects to the map. MapLibre adds a source, then one or more layers that draw it. It is more code for a single polyline and much less for a thousand, because the styling is declarative and the rendering is on the GPU. Expect to restructure anything that draws more than a handful of features.
Everything in Vector Tiles in MapLibre GL JS and the MapLibre examples works on this map.
With Leaflet
Leaflet keeps Google’s object model and its coordinate order, so most calls translate one to one. The basemap is a tile layer with a Maptoolkit raster style.
- <script src="https://maps.googleapis.com/maps/api/js?key=GOOGLE_KEY"></script>
+ <script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script>
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css" />
- const map = new google.maps.Map(document.getElementById("map"), {
- center: { lat: 48.21, lng: 16.37 },
- zoom: 12,
- });
+ const map = L.map("map").setView([48.21, 16.37], 12);
+ L.tileLayer(`https://rtc-cdn.maptoolkit.net/rtc/maptoolkit-maptoolkit.summer/{z}/{x}/{y}{r}.png?api_key=${API_KEY}`, {
+ maxZoom: 18,
+ attribution: "© <a href='https://www.maptoolkit.com'>Maptoolkit</a> © <a href='https://www.openstreetmap.org/copyright'>OSM</a>",
+ }).addTo(map);
| Leaflet | |
|---|---|
new google.maps.Marker({position, map}) | L.marker([lat, lng]).addTo(map) |
new google.maps.InfoWindow({content}) | marker.bindPopup(content) or L.popup() |
map.setCenter(), map.setZoom() | map.setView([lat, lng], zoom) |
map.panTo() | map.panTo([lat, lng]) or map.flyTo([lat, lng]) |
map.fitBounds(LatLngBounds) | map.fitBounds([[s, w], [n, e]]) |
google.maps.event.addListener(map, "click", fn) | map.on("click", fn) |
e.latLng.lat(), e.latLng.lng() | e.latlng.lat, e.latlng.lng |
new google.maps.Polyline({path}) | L.polyline(latlngs).addTo(map) |
new google.maps.Data() | L.geoJSON(data).addTo(map) |
| MarkerClusterer | Leaflet.markercluster |
styles: [...] on the map options | one of the raster styles, or vector tiles in Leaflet |
Leaflet’s zoom steps are whole numbers by default, like Google’s, and its bounds are
[[south, west], [north, east]], latitude first. The
Leaflet examples cover clustering, GeoJSON styling,
routing, geocoding and more on Maptoolkit tiles.
Markers at scale
If you are drawing more than a few hundred markers, do not port Marker to
maplibregl.Marker. Each one is a DOM element in both libraries, and the performance ceiling
is the same. Put the points in a GeoJSON source with a symbol or circle layer and let the
GPU draw them. This is usually the single biggest rendering improvement available in the move,
and it is easy to miss because the naive translation works fine at demo scale.
In Leaflet the same applies to L.marker. Use L.circleMarker with preferCanvas: true on the
map, which draws on one canvas instead of creating an element per point, or
cluster the markers.
The service APIs
Each is a small rewrite of request and response shapes rather than a URL swap:
- Routing API for Directions
- Matrix API for Distance Matrix
- Geocoding API for Geocoding
- Elevation API for Elevation
Two shape differences worth knowing before you start. Google wraps every response in a
status field that you check before reading results; Maptoolkit uses the HTTP status and
returns the payload directly. And Google returns an encoded polyline inside a nested
routes[].legs[].steps[] structure, where the Routing API returns the geometry at
paths[0].points.
Common problems after the switch
- Errors are
text/plain. A missing key returns403 Access denied!, an unrecognised key403 Api-key not found!, a key without access to that service403 Api-key not authorized!. Google returns a JSON body with astatusand anerror_message. Code that readsresponse.json()on a failure will throw. - There is no
status: "ZERO_RESULTS". An empty result is an empty result, expressed in the payload rather than in a status string. - Attribution is a license condition, not a logo you can restyle away. Maptoolkit and
OpenStreetMap stay visible. In Leaflet, set them in the tile layer’s
attributionoption. In MapLibre, the map’s attribution control defaults tocompact: true, which collapses the credit behind a button as soon as the map is dragged. Setcompact: falseto keep it readable. - Billing is per request, not per map load or per session. There is no session-token concept, so the autocomplete billing model has no analogue. A like-for-like estimate needs your tile volume, which a map-load count does not give you.
- Place IDs do not survive. Anything you have stored as a Google place ID is not resolvable here. If your database keys on place IDs, plan to re-geocode.
Doing it in stages
- Move the service APIs first, one at a time, behind a flag. They are independent of the map and each is a contained change.
- Pick the library, then build the new map alongside the old one on a single page, so the two can be compared directly.
- Write the coordinate conversion once, at the boundary, and use it everywhere.
- Restructure marker and overlay code onto sources and layers before porting features.
- Re-geocode anything keyed on a Google place ID.
- Check attribution renders on every map, at every breakpoint, after a drag.
- Run both for a full billing period before turning Google off.
Pricing
Self-service plans are billed through RapidAPI. See RapidAPI for how to subscribe and authenticate. Plan allowances and prices are on the Maptoolkit pricing page. Enterprise customers call the native hosts with a Maptoolkit API key.
Tile requests and API requests are metered separately and at different rates.