From Esri
Esri is a Map plus a MapView, a World Geocoding Service, and a Network Analyst stack (Route, Service Area, and a dozen siblings). unmap is one key and three clients. The jobs that are a map, a search, or a route map cleanly. The rest of the ArcGIS platform does not come with you.
Stay on Esri if the app is an ArcGIS Online organisation: hosted feature layers, a web map you did not write, Living Atlas, SceneView, or anything that talks to a FeatureServer. unmap has no equivalent for those, and pretending otherwise would waste a week. Everything below assumes you are replacing a Canadian basemap, a Canadian locator, or a Canadian route.
The map
ArcGIS Maps SDK for JavaScript builds a Map (the basemap) and a MapView (the camera). unmap builds both in one call and hands back a MapLibre map.
esriConfig.apiKey = "ESRI_API_KEY";
const map = new Map({ basemap: "arcgis/streets" });
const view = new MapView({
container: "viewDiv",
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,
});import { Unmap } from "@unmap/sdk";
const unmap = new Unmap({
key: "um_live_...",
container: "map",
center: [-114.0719, 51.0447],
zoom: 11,
});MapView.center is already [longitude, latitude]. You do not flip it. arcgis/streets becomes style: "base". arcgis/topographic and arcgis/outdoor are closest to outdoor. arcgis/dark-gray and arcgis/light-gray are muted with mode: "dark" or mode: "light". There is no imagery basemap. The twelve styles are on Maps API.
Widgets you added with view.ui.add (Search, Directions, BasemapToggle, Legend) do not exist as drop-ins. Zoom and compass are already on the map. Search and routing are the two clients below, and you render the list.
Graphics and popups
A Graphic on view.graphics becomes a MapLibre Marker. A PopupTemplate becomes a Popup. For a FeatureLayer of hundreds of points, add them as a GeoJSON source instead: WebGL draws those, the DOM does not.
view.graphics.add(
new Graphic({
geometry: { type: "point", longitude: -114.0719, latitude: 51.0447 },
symbol: { type: "simple-marker", color: "red" },
popupTemplate: { title: "Calgary Tower" },
}),
);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!);view.graphics.remove(graphic) becomes marker.remove(). A FeatureLayer URL (…/FeatureServer/0) has no unmap twin: export the features as GeoJSON and add a source, or keep that layer on Esri.
Geocoding
The World Geocoding Service (findAddressCandidates, suggest, reverseGeocode) and locator.addressToLocations become unmap.geocoder. There is no Search widget to drop in.
const url = new URL("https://geocode-api.arcgis.com/arcgis/rest/services/World/GeocodeServer/findAddressCandidates");
url.searchParams.set("address", "Calgary Tower");
url.searchParams.set("f", "pjson");
url.searchParams.set("token", "ESRI_API_KEY");
const data = await (await fetch(url)).json();
const { x, y } = data.candidates[0].location;
view.goTo([x, y]);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 });
const here = await unmap.geocoder.reverse(-114.07, 51.05);
const nearby = await unmap.geocoder.nearby("pharmacy", {
near: [-114.07, 51.05],
radius: 1000,
});import { Unmap } from "@unmap/sdk";
const unmap = new Unmap({ key: "um_live_..." });
const results = await unmap.geocoder.search("Calgary Tower", { limit: 3 });Esri candidates carry address, location: { x, y }, score, and attributes (Addr_type, PlaceName, …). unmap results carry id, name, layer, lng, lat, names, and a flat address. There is no magicKey to pass from suggest to find, no batch geocodeAddresses, and no ArcGIS Places contacts or hours. Category search is nearby, not a category filter on the locator. The Geocoding API is the contract.
location on Esri is { x, y } in the output spatial reference, usually WGS84 lon/lat. That pair is already [lng, lat].
Routing and service areas
ArcGIS Route (/Route/NAServer/Route_World/solve) and Service Area become router.route and router.isochrone. Esri's stops are x,y (longitude first). The geometry Esri returns is often compressed or in Web Mercator; unmap returns a WGS84 GeoJSON LineString.
const url = new URL("https://route-api.arcgis.com/arcgis/rest/services/World/Route/NAServer/Route_World/solve");
url.searchParams.set("stops", "-114.0719,51.0447;-113.9871,51.0899");
url.searchParams.set("f", "pjson");
url.searchParams.set("token", "ESRI_API_KEY");
const data = await (await fetch(url)).json();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. Walking is pedestrian. Cycling is bicycle. Truck takes a vehicle profile in metres and tonnes, not Esri's attribute parameters on a travel mode. There are no extra stops: a request is exactly two points. There is no returnDirections text, no barriers, and no time windows.
Service Area is the same idea as router.isochrone, with a smaller surface: up to four contours, 120 minutes each.
const bands = await unmap.router.isochrone([-114.07, 51.05], {
minutes: [10, 20],
mode: "auto",
});Closest Facility, OD Cost Matrix, Location-Allocation, and Fleet Routing have no unmap twin. A matrix is repeated route calls, and you will feel the meter. The Routing API documents modes, the truck profile, and what fails outside Canada.
What has no equivalent
- Feature layers, MapServer, FeatureServer, and hosted layers. Export to GeoJSON or keep them on Esri.
- ArcGIS Online web maps, Living Atlas, and Experience Builder.
- SceneView and 3D.
- Imagery and hybrid basemaps.
- Spatial analysis as a service (buffer, intersect, geoenrichment).
- Batch geocoding, Places contacts, and magicKey suggest.
- Closest Facility, OD matrix, Location-Allocation, barriers, and time windows.
- Named users, groups, and an Enterprise portal.
- Coverage outside Canada.
If the app is GIS, not a map in a product, stay on Esri.
Checklist
- Esri API key or named-user session replaced with
um_live_.../um_test_... -
Map+MapViewreplaced withnew Unmap({ container, ... }) - Graphics and popups on
unmap.map - Locator, suggest, and reverse pointed at
unmap.geocoder - Route and Service Area pointed at
unmap.router, with the line drawn as GeoJSON - Feature layers either exported or left on Esri
- Web maps, Living Atlas, and SceneView either dropped or left on Esri
- 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.