Skip to content

Both copy text to your clipboard — Build with AI copies a setup prompt to paste into Claude Code, Cursor, Codex or Copilot; Copy page as Markdown copies this page to paste into a chat. How it works

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-sdk

Select the Swift package directory and add the MapAtlasGeocoder product to your target.

Package.swift

swift
dependencies: [
    .package(url: "https://github.com/MapMetrics/geocoder-sdk", branch: "main"),
],
targets: [
    .target(
        name: "YourTarget",
        dependencies: [
            .product(name: "MapAtlasGeocoder", package: "geocoder-sdk"),
        ]
    ),
]

Quickstart

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

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

swift
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:):

swift
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, no Origin header sent. See Choosing a key for a Swift app.
  • .originNotAllowed — request's Origin isn't on the key's allow-list.
  • .quotaExceeded — OSM tier cap exhausted; carries selfHostURL.
  • .tierUnsupported — thrown by createSession() on the .osm tier and on MapAtlas.selfHosted(baseURL:) clients, and by autocomplete(_:) on the .v2 tier.
  • .notRetrievable — thrown by retrieve(_:)/retrieveBatch(_:) when a Suggestion has no ord (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.
swift
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: