From Google Maps
Google Maps Platform is three products in one key: a map, Places, and Directions. unmap is the same three jobs, over Canada, on a MapLibre map. The renderer changes. The calls get shorter. The coordinate order flips.
Stay on Google if Street View, Place photos, or a map outside Canada is load-bearing. unmap has none of those. Everything below assumes you are replacing a Canadian map, a Canadian search, or a Canadian route.
Coming from a <script> tag? unmap loads from one too, no build step required: see the Quickstart.
The map
Google builds an imperative Map and talks to it with setters. unmap builds a MapLibre map and hands it back. After that, MapLibre's own API is the map API.
const map = new google.maps.Map(document.getElementById("map"), {
center: { lat: 51.0447, lng: -114.0719 },
zoom: 11,
mapTypeId: "roadmap",
});import { Unmap } from "@unmap/sdk";
const unmap = new Unmap({
key: "um_live_...",
container: "map",
center: [-114.0719, 51.0447],
zoom: 11,
});import { Unmap } from "@unmap/sdk";
const unmap = new Unmap({
key: "um_live_...",
container: "map",
center: [-114.0719, 51.0447],
zoom: 11,
});roadmap becomes style: "base". hybrid and satellite have no equivalent: unmap serves vector tiles, not imagery. terrain is the outdoor style, which adds hillshade and contours. Twelve styles, each in light and dark, are on Maps API.
Import MapLibre's stylesheet (import "maplibre-gl/dist/maplibre-gl.css") and give the container a height. A box with no height is a map of zero pixels, and MapLibre reports no error.
Coordinates
Google uses { lat, lng } and latLng.lat(). unmap uses [lng, lat], longitude first, the GeoJSON order. Swap them and the map lands in the Indian Ocean.
| Google Maps | unmap |
|---|---|
{ lat: 51.0447, lng: -114.0719 } | [-114.0719, 51.0447] |
map.setCenter({ lat, lng }) | unmap.map.setCenter([lng, lat]) |
map.setZoom(12) | unmap.map.setZoom(12) |
map.panTo({ lat, lng }) | unmap.map.panTo([lng, lat]) |
map.fitBounds(bounds) | unmap.map.fitBounds([[lng, lat], [lng, lat]]) |
mapTypeId: "roadmap" | style: "base" |
google.maps.event.addListener | unmap.map.on("click", fn) |
event.latLng | event.lngLat |
unmap.map is the MapLibre GL map. Camera, events, sources, and layers are documented upstream, not here.
Markers and info windows
Google creates a Marker per point and an InfoWindow per click. MapLibre does the same job with Marker and Popup. For more than a hundred points, add them as a GeoJSON source instead: WebGL draws those, the DOM does not.
const marker = new google.maps.Marker({
position: { lat: 51.0447, lng: -114.0719 },
map,
title: "Calgary Tower",
});
const info = new google.maps.InfoWindow({ content: "<p>Calgary Tower</p>" });
marker.addListener("click", () => info.open(map, marker));import { maplibregl } from "@unmap/sdk";
new maplibregl.Marker()
.setLngLat([-114.0719, 51.0447])
.setPopup(new maplibregl.Popup().setHTML("<p>Calgary Tower</p>"))
.addTo(unmap.map!);import { Unmap, maplibregl } from "@unmap/sdk";
const unmap = new Unmap({
key: "um_live_...",
container: "map",
center: [-114.0719, 51.0447],
zoom: 11,
});
const [place] = await unmap.geocoder.search("Calgary Tower");
new maplibregl.Marker({ color: "#FF3E9A" })
.setLngLat([place.lng, place.lat])
.setPopup(new maplibregl.Popup().setText(place.name))
.addTo(unmap.map!);marker.setMap(null) becomes marker.remove(). There is no AdvancedMarkerElement; a custom icon is an HTML element passed to new maplibregl.Marker({ element }), or a symbol layer for large sets.
Search and autocomplete
Places findPlaceFromQuery and the Autocomplete widget become geocoder.search and geocoder.autocomplete. Both return a JSON array. There is no Places widget to drop in: you render the list.
const geocoder = new google.maps.Geocoder();
geocoder.geocode({ address: "Calgary Tower" }, (results, status) => {
if (status === "OK") map.setCenter(results[0].geometry.location);
});const results = await unmap.geocoder.search("Calgary Tower");
const [first] = results;
if (first) unmap.map!.setCenter([first.lng, first.lat]);const hints = await unmap.geocoder.autocomplete("calgary tow", { limit: 5 });import { Unmap } from "@unmap/sdk";
const unmap = new Unmap({ key: "um_live_..." });
const results = await unmap.geocoder.search("Calgary Tower", { limit: 3 });A result is id, name, layer, lng, lat, plus names and address when they exist. There is no place_id you can reuse on Google later, and no Place photos, reviews, or opening hours. The full shape is on the Geocoding API.
AutocompleteService session tokens and per-keystroke billing do not apply. Every request is one call on one meter; see Plans & Limits.
Nearby search
Places Nearby Search (type: "pharmacy") is geocoder.nearby. Pass a category word in English or French, or a taxonomy id such as health.pharmacy.
const service = new google.maps.places.PlacesService(map);
service.nearbySearch({ location: { lat: 51.05, lng: -114.07 }, radius: 1000, type: "pharmacy" }, handle);const nearby = await unmap.geocoder.nearby("pharmacy", {
near: [-114.07, 51.05],
radius: 1000,
limit: 5,
});Results arrive sorted by distance, with distance in metres on each row. There is no rank-by-prominence, no price level, and no Place Details follow-up.
Directions
DirectionsService plus DirectionsRenderer become one router.route call and a GeoJSON line you add yourself.
const directions = new google.maps.DirectionsService();
directions.route(
{
origin: { lat: 51.0447, lng: -114.0719 },
destination: { lat: 51.0899, lng: -113.9871 },
travelMode: "DRIVING",
},
(res, status) => {
if (status === "OK") renderer.setDirections(res);
},
);const route = await unmap.router.route([-114.0719, 51.0447], [-113.9871, 51.0899], {
mode: "auto",
});
unmap.map!.addSource("route", {
type: "geojson",
data: { type: "Feature", properties: {}, geometry: route.geometry },
});
unmap.map!.addLayer({
id: "route",
type: "line",
source: "route",
paint: { "line-color": "#3887be", "line-width": 4 },
});import { Unmap } from "@unmap/sdk";
const unmap = new Unmap({
key: "um_live_...",
container: "map",
center: [-114.03, 51.07],
zoom: 11,
});
const route = await unmap.router.route([-114.0719, 51.0447], [-113.9871, 51.0899], { mode: "auto" });
unmap.map!.on("load", () => {
unmap.map!.addSource("route", {
type: "geojson",
data: { type: "Feature", properties: {}, geometry: route.geometry },
});
unmap.map!.addLayer({
id: "route-line",
type: "line",
source: "route",
layout: { "line-cap": "round", "line-join": "round" },
paint: { "line-color": "#FF3E9A", "line-width": 3 },
});
});DRIVING is auto (or car, the same profile). WALKING is pedestrian. BICYCLING is bicycle. TRANSIT has no equivalent. There are no waypoints: a request is exactly two points. distanceMeters and durationSeconds are metres and seconds, not the text Google puts on the route.
Travel-time areas are router.isochrone, which Google does not ship as a first-class API:
const bands = await unmap.router.isochrone([-114.07, 51.05], { minutes: [10, 20] });The Routing API documents modes, the truck profile, and what fails outside Canada.
What has no equivalent
- Street View. No panorama service.
- Place photos, reviews, opening hours, phone numbers. Geocoding returns a name, a coordinate, and a structured address.
- Distance Matrix and Roads. No travel-time matrix, no snap-to-road.
- Drawing Manager and heatmaps. MapLibre Draw and a heatmap layer are the MapLibre-side replacements; unmap does not wrap them.
- Worldwide coverage. Canada only. A route that leaves the country is a 400.
- Satellite and hybrid imagery.
If any of those are why the app exists, do not migrate that part.
Checklist
- Key from the dashboard, presented as
um_live_...orum_test_... -
{ lat, lng }flipped to[lng, lat]everywhere, including stored data - Map stylesheet imported and the container given a height
- Markers and popups on
unmap.map - Search, autocomplete, nearby, and reverse pointed at
unmap.geocoder - Directions pointed at
unmap.router, with the line drawn as GeoJSON - Street View, Place Details, and imagery either dropped or left on Google
- Origin configured for production traffic (Plans & Limits)
The Quickstart is the same map built from scratch. The Examples are one idea per page if you would rather copy a working file than translate one.