Swift SDK
MapAtlasGeocoder is a Swift client for the v2 geocoding API — autocomplete, place retrieval, and forward/reverse geocoding — for iOS and macOS. Foundation only, no third-party dependencies, async/await throughout.
Not published yet
Not on the Swift Package Index yet. Add it as a git-based Swift Package today — see below; the coordinates don't change once it's indexed.
Install
Platforms: macOS 12+, iOS 15+.
Xcode
File → Add Package Dependencies… and enter:
https://github.com/MapMetrics/geocoder-sdkSelect the Swift package directory and add the MapAtlasGeocoder product to your target.
Package.swift
dependencies: [
.package(url: "https://github.com/MapMetrics/geocoder-sdk", branch: "main"),
],
targets: [
.target(
name: "YourTarget",
dependencies: [
.product(name: "MapAtlasGeocoder", package: "geocoder-sdk"),
]
),
]Quickstart
import MapAtlasGeocoder
let mapatlas = MapAtlas(token: "YOUR_API_KEY")
// One session covers a whole search — every keystroke, then the pick.
let session = try mapatlas.geocoding.createSession()
// As the user types. Debounced for you; suggestions carry no coordinates.
let results = try await session.suggest("Nieuwezijds")
// When they pick one, hand back the suggestion — not an id.
let place = try await session.retrieve(results[0])
print(place.latitude, place.longitude)
// Resolving several at once is both fewer round trips and cheaper billing —
// see Sessions & Billing.
let places = try await session.retrieveBatch(Array(results.prefix(3)))
// One-shot lookups, when there's no user typing to debounce. Both return GeoJSON.
let forward = try await mapatlas.geocoding.search("Nieuwezijds Voorburgwal 147", country: "nl")
let reverse = try await mapatlas.geocoding.reverse(lat: 52.37, lon: 4.89)The reactive layer: GeocodeSearchController
GeocodeSearchController is a headless, @Observable, @MainActor controller — bind your own markup to it. Requires macOS 14 / iOS 17 (the rest of the package works down to macOS 12 / iOS 15 — @Observable alone needs the newer OS).
import SwiftUI
import MapAtlasGeocoder
struct AddressField: View {
@State private var controller: GeocodeSearchController
var onSelect: (RetrievedPlace) -> Void
init(client: MapAtlas, onSelect: @escaping (RetrievedPlace) -> Void) {
_controller = State(initialValue: try! GeocodeSearchController(client: client, country: "nl"))
self.onSelect = onSelect
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
TextField("Search an address…", text: $controller.query)
.textFieldStyle(.roundedBorder)
if controller.isLoading {
ProgressView()
}
if let error = controller.error {
Text(error.message).foregroundStyle(.red).font(.caption)
}
List(controller.suggestions, id: \.text) { suggestion in
Button(suggestion.placeName ?? suggestion.text ?? "") {
Task {
if let place = await controller.select(suggestion) {
onSelect(place)
}
}
}
// Suggestions can be non-retrievable (e.g. a bare "Amsterdam"
// locality row) — still shown, but selecting one surfaces
// `.notRetrievable` into `controller.error` instead of a crash.
.disabled(!suggestion.isRetrievable)
}
}
}
}Guarantees: one Session is created for the controller's lifetime and reused across every keystroke; select(_:) retrieves and closes it, and the next query change reopens it automatically. minLength (default 2) is enforced locally — no request, no session activity, below it. Out-of-order responses are discarded: each search is tagged with a sequence number, and a slow response for an earlier keystroke arriving after a newer one is never applied. Errors land in controller.error, never thrown into a view.
The OSM tier
tier: .osm talks to the free, OpenStreetMap-only endpoints (/osm-geocode/, /osm-reverse/, /osm-autocomplete/) via the osm-geocode scope. No session/retrieve flow — search(), reverse(), and autocomplete(_:) are the only three operations. Calling createSession() on this tier throws .tierUnsupported. Rate-limited to 10,000 requests/key/day plus a global monthly cap; exhausting it throws .quotaExceeded with a selfHostURL.
let mapatlas = MapAtlas(token: "YOUR_KEY", tier: .osm)
let result = try await mapatlas.geocoding.search("Nieuwezijds Voorburgwal 147")
if case .osm(let raw) = result {
// raw: OsmResponse (= JSONValue) — narrow it yourself at the call site.
}autocomplete(_:) on this tier is the opposite of v2 autocomplete: no session, no retrieve() step, results already carry coordinates. It's only available with tier: .osm — on the default .v2 tier it throws .tierUnsupported and points you at createSession() + suggest(_:) instead.
Self-hosting
The free tier's engine is open source: MapMetrics/atlas-osm-geocoder, deployable on Cloudflare Workers with a prebuilt planet bundle. Point this package at your own instance with MapAtlas.selfHosted(baseURL:):
let mapatlas = MapAtlas.selfHosted(baseURL: URL(string: "https://my-worker.workers.dev")!)
try await mapatlas.geocoding.search("Nieuwezijds Voorburgwal 147")
try await mapatlas.geocoding.autocomplete("Nieuwezijds")
try await mapatlas.geocoding.reverse(lat: 52.37, lon: 4.89)A self-hosted instance takes no token and uses different paths (/search, /reverse, /autocomplete, no token parameter) — there is no token/getToken option on this constructor at all, since a self-hosted instance has no auth and this package never sends one. createSession() (and session-based retrieval) throw .tierUnsupported here too — this engine has no session concept at all.
Choosing a key for a Swift app
Origin restriction is a browser-only feature — it's enforced by checking the Origin header a browser sends, and native apps never send one. A native app can never satisfy an origin-restricted key — this is the error you'll hit if you reuse a browser-restricted key in an iOS app. Use an unrestricted key for iOS/macOS instead. See API Keys & Security for the full model.
Errors
MapAtlasError is a Swift enum with one case per failure mode. Switch on it, or read .message / .status / .code / .name:
.tokenNotFound— key was never provisioned..tokenInactive— key exists but is deactivated..scopeNotAllowed— key is valid but not scoped for this operation..originRequired— key is origin-restricted, noOriginheader sent. See Choosing a key for a Swift app..originNotAllowed— request'sOriginisn't on the key's allow-list..quotaExceeded— OSM tier cap exhausted; carriesselfHostURL..tierUnsupported— thrown bycreateSession()on the.osmtier and onMapAtlas.selfHosted(baseURL:)clients, and byautocomplete(_:)on the.v2tier..notRetrievable— thrown byretrieve(_:)/retrieveBatch(_:)when aSuggestionhas noord(isRetrievable == false)..network— the request never completed, or the response wasn't parseable JSON..decoding— the response was valid JSON but didn't match the expected shape..unknown— any other non-2xx response, not covered above.
import MapAtlasGeocoder
do {
_ = try await mapatlas.geocoding.search("Nieuwezijds Voorburgwal 147", country: "nl")
} catch let error as MapAtlasError {
switch error {
case .originRequired:
// this key can't be used from a native app — swap it for an unrestricted one
break
case .quotaExceeded(_, _, _, let selfHostURL):
print("quota exceeded, self-host at:", selfHostURL as Any)
default:
print(error.name, error.message)
}
}Not implemented yet
This package covers geocoding only — routing, matrix, map matching, optimization, and isochrones are not implemented here. The TypeScript and Dart SDKs cover those endpoints; see JavaScript or Flutter if you need routing from a shared backend or a cross-platform Dart layer.
Endpoint reference
This SDK is a typed wrapper — for the full parameter list, response shape, and gateway-level gotchas each call is protecting you from, see: