home / skills / rshankras / claude-code-apple-skills / geotoolbox

geotoolbox skill

/skills/mapkit/geotoolbox

This skill helps you work with PlaceDescriptor patterns for geocoding, MapKit integration, and multi-service location data.

npx playbooks add skill rshankras/claude-code-apple-skills --skill geotoolbox

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
12.1 KB
---
name: geotoolbox
description: GeoToolbox PlaceDescriptor patterns with MapKit integration for location representation, geocoding, and multi-service place identifiers. Use when working with place descriptors, geocoding, or cross-service location data.
allowed-tools: [Read, Glob, Grep]
---

# GeoToolbox and PlaceDescriptor Patterns

Portable location representation using `PlaceDescriptor` from the GeoToolbox framework. Covers place construction from coordinates, addresses, and MapKit items; forward and reverse geocoding with the new async APIs; and multi-service place identifiers for cross-platform interop.

## When This Skill Activates

Use this skill when the user:
- Asks about **GeoToolbox** or **PlaceDescriptor**
- Wants to represent a place with coordinates, address, or common name
- Needs to convert between **MKMapItem** and **PlaceDescriptor**
- Asks about **forward geocoding** (address to coordinates) or **reverse geocoding** (coordinates to address)
- Wants to store or transmit **multi-service place identifiers** (Apple Maps, Google Maps, etc.)
- Mentions **PlaceRepresentation**, **SupportingPlaceRepresentation**, or **MKGeocodingRequest**
- Asks about portable or interoperable location data structures

## Decision Tree

```
What do you need?
|
+-- Represent a place with coordinates and/or address
|   +-- From a known coordinate
|   |   +-- PlaceRepresentation.coordinate(CLLocationCoordinate2D)
|   +-- From a known address string
|   |   +-- PlaceRepresentation.address(String)
|   +-- Both coordinate and address
|   |   +-- Pass multiple representations to PlaceDescriptor
|   +-- From an existing MKMapItem
|       +-- PlaceDescriptor(item: MKMapItem)
|
+-- Geocode an address to coordinates
|   +-- MKGeocodingRequest(addressString:)
|   +-- try await request.mapItems
|
+-- Reverse geocode coordinates to an address
|   +-- MKReverseGeocodingRequest(location:)
|   +-- try await request.mapItems
|
+-- Attach service identifiers (Apple Maps, Google, etc.)
|   +-- SupportingPlaceRepresentation.serviceIdentifiers([String: String])
|
+-- Read place properties
    +-- descriptor.coordinate, descriptor.address, descriptor.commonName
    +-- descriptor.serviceIdentifier(for: "com.apple.maps")
```

## API Availability

| API | Minimum OS | Import |
|-----|-----------|--------|
| `PlaceDescriptor` | iOS 26 / macOS 26 | `GeoToolbox` |
| `PlaceRepresentation` | iOS 26 / macOS 26 | `GeoToolbox` |
| `SupportingPlaceRepresentation` | iOS 26 / macOS 26 | `GeoToolbox` |
| `MKGeocodingRequest` | iOS 26 / macOS 26 | `MapKit` |
| `MKReverseGeocodingRequest` | iOS 26 / macOS 26 | `MapKit` |
| `PlaceDescriptor(item:)` | iOS 26 / macOS 26 | `GeoToolbox` + `MapKit` |
| `MKMapItem` | iOS 6 / macOS 10.9 | `MapKit` |
| `CLLocationCoordinate2D` | iOS 2 / macOS 10.6 | `CoreLocation` |

## Quick Start

### Create a PlaceDescriptor from Coordinates

```swift
import GeoToolbox
import CoreLocation

let coordinate = CLLocationCoordinate2D(latitude: 37.3349, longitude: -122.0090)

let descriptor = PlaceDescriptor(
    representations: [.coordinate(coordinate)],
    commonName: "Apple Park"
)

// Read back
if let coord = descriptor.coordinate {
    print("Lat: \(coord.latitude), Lon: \(coord.longitude)")
}
print(descriptor.commonName ?? "No name")
```

### Create a PlaceDescriptor from an Address

```swift
import GeoToolbox

let descriptor = PlaceDescriptor(
    representations: [.address("One Apple Park Way, Cupertino, CA 95014")],
    commonName: "Apple Park"
)

if let address = descriptor.address {
    print("Address: \(address)")
}
```

### Multiple Representations and Service Identifiers

```swift
import GeoToolbox
import CoreLocation

let coordinate = CLLocationCoordinate2D(latitude: 37.3349, longitude: -122.0090)

let descriptor = PlaceDescriptor(
    representations: [
        .coordinate(coordinate),
        .address("One Apple Park Way, Cupertino, CA 95014")
    ],
    commonName: "Apple Park",
    supportingRepresentations: [
        .serviceIdentifiers([
            "com.apple.maps": "apple-maps-id-12345",
            "com.google.maps": "ChIJ-bfVTh8_j4ARDMPaL2Njo3I"
        ])
    ]
)

// Access a specific service identifier
if let appleId = descriptor.serviceIdentifier(for: "com.apple.maps") {
    print("Apple Maps ID: \(appleId)")
}
```

### Convert from MKMapItem

```swift
import GeoToolbox
import MapKit

func descriptorFromMapItem(_ mapItem: MKMapItem) -> PlaceDescriptor {
    PlaceDescriptor(item: mapItem)
}
```

## Forward Geocoding (Address to Coordinates)

```swift
import MapKit

func geocodeAddress(_ addressString: String) async throws -> [MKMapItem] {
    let request = MKGeocodingRequest(addressString: addressString)
    let mapItems = try await request.mapItems
    return mapItems
}

// Usage
let items = try await geocodeAddress("One Apple Park Way, Cupertino, CA")
if let first = items.first {
    let coord = first.placemark.coordinate
    print("Found: \(coord.latitude), \(coord.longitude)")
}
```

## Reverse Geocoding (Coordinates to Address)

```swift
import MapKit
import CoreLocation

func reverseGeocode(_ coordinate: CLLocationCoordinate2D) async throws -> [MKMapItem] {
    let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
    let request = MKReverseGeocodingRequest(location: location)
    let mapItems = try await request.mapItems
    return mapItems
}

// Usage
let coordinate = CLLocationCoordinate2D(latitude: 37.3349, longitude: -122.0090)
let items = try await reverseGeocode(coordinate)
if let first = items.first {
    print("Address: \(first.placemark.title ?? "Unknown")")
}
```

## Full Integration Example

Geocode an address, convert the result to a PlaceDescriptor with service identifiers, and read back all properties:

```swift
import GeoToolbox
import MapKit
import CoreLocation

func buildPlaceDescriptor(from addressString: String) async throws -> PlaceDescriptor? {
    // Forward geocode
    let request = MKGeocodingRequest(addressString: addressString)
    let mapItems = try await request.mapItems
    guard let mapItem = mapItems.first else { return nil }

    // Convert MKMapItem to PlaceDescriptor
    var descriptor = PlaceDescriptor(item: mapItem)

    // Or build manually with extra data
    let coordinate = mapItem.placemark.coordinate
    descriptor = PlaceDescriptor(
        representations: [
            .coordinate(coordinate),
            .address(addressString)
        ],
        commonName: mapItem.name,
        supportingRepresentations: [
            .serviceIdentifiers([
                "com.apple.maps": "resolved-id-\(coordinate.latitude)"
            ])
        ]
    )

    return descriptor
}

func displayDescriptor(_ descriptor: PlaceDescriptor) {
    if let name = descriptor.commonName {
        print("Name: \(name)")
    }
    if let coord = descriptor.coordinate {
        print("Coordinate: \(coord.latitude), \(coord.longitude)")
    }
    if let address = descriptor.address {
        print("Address: \(address)")
    }
    if let appleId = descriptor.serviceIdentifier(for: "com.apple.maps") {
        print("Apple Maps ID: \(appleId)")
    }
}
```

## Top Mistakes

| # | Mistake | Problem | Fix |
|---|---------|---------|-----|
| 1 | Importing only `MapKit` when using `PlaceDescriptor` | `PlaceDescriptor` lives in `GeoToolbox`, not `MapKit` | Add `import GeoToolbox` alongside `import MapKit` |
| 2 | Using `CLGeocoder` instead of `MKGeocodingRequest` | `CLGeocoder` returns `CLPlacemark` which lacks MapKit integration | Use `MKGeocodingRequest` / `MKReverseGeocodingRequest` for `MKMapItem` results |
| 3 | Assuming `PlaceDescriptor` always has a coordinate | A descriptor can be address-only with no coordinate | Check `descriptor.coordinate` for `nil` before use |
| 4 | Assuming `PlaceDescriptor` always has an address | A descriptor can be coordinate-only with no address | Check `descriptor.address` for `nil` before use |
| 5 | Hardcoding service identifier keys | Service identifier keys are strings; typos cause silent failures | Define constants for service keys like `"com.apple.maps"` |
| 6 | Passing `CLLocationCoordinate2D` directly to `MKReverseGeocodingRequest` | The initializer takes a `CLLocation`, not a raw coordinate | Wrap in `CLLocation(latitude:longitude:)` first |
| 7 | Ignoring empty geocoding results | Geocoding can return zero results for ambiguous or invalid input | Guard against empty `mapItems` arrays |
| 8 | Not handling geocoding errors | Network or service failures throw errors | Use `do/catch` or `try await` with proper error handling |

## Patterns

### Service Identifier Constants

Define constants to avoid typos in service identifier keys:

```swift
// Good -- constants prevent typos
enum PlaceService {
    static let appleMaps = "com.apple.maps"
    static let googleMaps = "com.google.maps"
    static let foursquare = "com.foursquare"
}

if let id = descriptor.serviceIdentifier(for: PlaceService.appleMaps) {
    // use id
}
```

```swift
// Bad -- raw string literals are error-prone
if let id = descriptor.serviceIdentifier(for: "com.apple.map") { // typo: "map" not "maps"
    // silently nil
}
```

### Nil-Safe Property Access

```swift
// Good -- check each optional property
func formatPlace(_ descriptor: PlaceDescriptor) -> String {
    var parts: [String] = []
    if let name = descriptor.commonName {
        parts.append(name)
    }
    if let address = descriptor.address {
        parts.append(address)
    }
    if let coord = descriptor.coordinate {
        parts.append("\(coord.latitude), \(coord.longitude)")
    }
    return parts.joined(separator: " -- ")
}
```

```swift
// Bad -- force-unwrapping optional properties
let name = descriptor.commonName! // crashes if nil
let coord = descriptor.coordinate! // crashes if no coordinate representation
```

### Geocoding with Fallback

```swift
// Good -- handle empty results and errors
func resolvePlace(_ address: String) async -> PlaceDescriptor? {
    do {
        let request = MKGeocodingRequest(addressString: address)
        let items = try await request.mapItems
        guard let item = items.first else {
            print("No results for address: \(address)")
            return nil
        }
        return PlaceDescriptor(item: item)
    } catch {
        print("Geocoding failed: \(error.localizedDescription)")
        return nil
    }
}
```

```swift
// Bad -- no error handling, no empty check
func resolvePlace(_ address: String) async -> PlaceDescriptor {
    let request = MKGeocodingRequest(addressString: address)
    let items = try! await request.mapItems // crashes on failure
    return PlaceDescriptor(item: items.first!) // crashes if empty
}
```

## Review Checklist

- [ ] `import GeoToolbox` is present when using `PlaceDescriptor`, `PlaceRepresentation`, or `SupportingPlaceRepresentation`
- [ ] `import MapKit` is present when using `MKGeocodingRequest`, `MKReverseGeocodingRequest`, or `MKMapItem`
- [ ] `import CoreLocation` is present when using `CLLocationCoordinate2D` or `CLLocation`
- [ ] `PlaceDescriptor` properties (`coordinate`, `address`, `commonName`) are checked for `nil` before use
- [ ] Service identifier keys use defined constants, not raw string literals
- [ ] `MKReverseGeocodingRequest` receives a `CLLocation`, not a raw `CLLocationCoordinate2D`
- [ ] Geocoding results are checked for empty arrays before accessing elements
- [ ] Geocoding calls use proper `async/await` error handling with `do/catch`
- [ ] Multiple representations are provided when both coordinate and address are known
- [ ] Supporting representations include service identifiers when cross-service interop is needed

## Cross-References

- For **MapKit map views** and annotations, see MapKit documentation
- For **CoreLocation** permissions and location updates, see CoreLocation documentation
- For **SwiftUI integration** with maps, see `Map` view in SwiftUI

## References

- [GeoToolbox Framework](https://developer.apple.com/documentation/geotoolbox)
- [PlaceDescriptor](https://developer.apple.com/documentation/geotoolbox/placedescriptor)
- [MKGeocodingRequest](https://developer.apple.com/documentation/mapkit/mkgeocodingrequest)
- [MKReverseGeocodingRequest](https://developer.apple.com/documentation/mapkit/mkreversegeocodingrequest)
- [MKMapItem](https://developer.apple.com/documentation/mapkit/mkmapitem)

Overview

This skill explains portable place representation using PlaceDescriptor from GeoToolbox and how to integrate it with MapKit for geocoding and cross-service identifiers. It shows how to build descriptors from coordinates, addresses, or MKMapItem, perform forward and reverse geocoding with async APIs, and attach multi-service place identifiers for interoperability.

How this skill works

PlaceDescriptor groups multiple PlaceRepresentation values (coordinate, address, commonName) and optional SupportingPlaceRepresentation entries (service identifiers). Use PlaceDescriptor(item: MKMapItem) to convert MapKit results directly. Perform forward and reverse geocoding with MKGeocodingRequest and MKReverseGeocodingRequest and convert MKMapItem results into PlaceDescriptor instances or build descriptors manually.

When to use it

  • You need a portable, serializable representation of a place for storage or network transfer.
  • Converting MKMapItem results from MapKit into a neutral place structure.
  • Forward geocoding: turn an address string into coordinates and a map item.
  • Reverse geocoding: resolve coordinates into an address or named place.
  • Maintaining cross-service identifiers (Apple Maps, Google, Foursquare) for the same place.

Best practices

  • Always import GeoToolbox when using PlaceDescriptor and MapKit for geocoding APIs.
  • Check optional properties (coordinate, address, commonName) for nil before use.
  • Prefer MKGeocodingRequest / MKReverseGeocodingRequest to get MKMapItem results rather than CLGeocoder/CLPlacemark.
  • Wrap CLLocationCoordinate2D in CLLocation for reverse geocoding requests.
  • Define constants for service identifier keys to avoid silent failures from typos.
  • Handle empty geocoding results and errors with async/await and do/catch.

Example use cases

  • Create a PlaceDescriptor from known coordinates and display its commonName and lat/lon.
  • Forward geocode a user-entered address, convert the first MKMapItem to a PlaceDescriptor, and attach service IDs.
  • Reverse geocode a tap on a map to show an address string and build a descriptor for sharing.
  • Migrate place data between mapping services by storing serviceIdentifiers in SupportingPlaceRepresentation.
  • Build a search result list by geocoding queries and formatting descriptors with nil-safe property access.

FAQ

Which imports are required to use PlaceDescriptor and geocoding?

Import GeoToolbox for PlaceDescriptor/representations, MapKit for MKGeocodingRequest and MKMapItem, and CoreLocation for CLLocation/CLLocationCoordinate2D.

Should I use CLGeocoder or MKGeocodingRequest?

Use MKGeocodingRequest / MKReverseGeocodingRequest when you need MKMapItem results and better MapKit integration; CLGeocoder returns CLPlacemark which lacks MapKit identifiers.