Sat Nov 08 Estimated time: PT60M
How to Create a Routing App Coverage File
Learn how to build and submit a routing app coverage file for iOS. A complete guide to GeoJSON coverage areas, App Store configuration, and integration with Apple Maps for transit and navigation apps.
What Is a Routing App Coverage File
A routing app coverage file is a GeoJSON document that tells Apple Maps where your app can provide directions. When a user requests transit, driving, walking, or cycling directions in Apple Maps, iOS checks the coverage files of all installed routing apps to determine which ones can handle the route. If your app covers the requested area, it appears as a routing option alongside Apple’s built-in directions.
This system was introduced in iOS 9 and applies to any app that provides turn-by-turn navigation, public transit directions, ride-hailing routing, cycling routes, or pedestrian navigation. Without a coverage file, your app will never appear in Apple Maps regardless of how good your routing engine is.
For App Store Optimization, the routing app integration is a significant discovery channel. Users who find your app through Apple Maps have extremely high intent - they need directions right now. This makes the routing integration one of the highest-converting organic channels available to navigation and transit apps.
Who Needs a Coverage File
You need a routing app coverage file if your app:
- Provides public transit directions (bus, subway, train, ferry schedules)
- Offers turn-by-turn driving navigation
- Provides cycling route planning
- Offers pedestrian walking directions
- Provides ride-hailing routing (showing routes for car services)
- Delivers any kind of point-to-point navigation that users might request through Maps
If your app only displays maps without routing (e.g., a map viewer or location tracker), you do not need a coverage file.
Step 1: Understand the Coverage File Requirements
Before building anything, understand exactly what Apple expects and how the system works.
How Apple Maps Uses Coverage Files
When a user requests directions in Apple Maps:
- Apple Maps determines the geographic area of the route (start point, end point, and intermediate area)
- It checks the coverage files of all installed apps that support routing
- Apps whose coverage area includes the route endpoints appear as routing options
- The user selects your app, and Apple Maps sends a routing request to it
- Your app opens and displays the directions
Your coverage file is the gatekeeper for step 2. If your GeoJSON polygons don’t include the user’s requested route, your app won’t appear even if it could technically handle the request.
Supported Routing Modes
Apple Maps supports these routing modes, defined in your Info.plist:
| Mode | Info.plist Key | Use case |
|---|---|---|
| Transit | MKDirectionsModesTransit | Bus, subway, train, ferry |
| Automobile | MKDirectionsModesAutomobile | Driving navigation |
| Pedestrian | MKDirectionsModesWalk | Walking directions |
| Cycling | MKDirectionsModeBicycle | Bike route planning |
| Ride share | MKDirectionsModesRideShare | Ride-hailing services |
Your coverage file can specify different routing modes for different geographic areas. For example, your app might support transit routing in 5 cities but cycling routing nationwide.
File Format
The coverage file must be valid GeoJSON conforming to RFC 7946. Apple accepts:
- Feature: A single polygon or multipolygon with properties
- FeatureCollection: Multiple features, each with their own geometry and properties
- File size limit: 20 MB (though smaller files process faster)
- Coordinate system: WGS 84 (EPSG:4326), the standard for GPS coordinates
- File extension:
.geojson
Step 2: Set Up Your Xcode Project
Your app needs the correct capabilities and Info.plist configuration before Apple Maps will recognize it as a routing provider.
Adding the Maps Capability
In Xcode:
- Select your app target
- Go to Signing & Capabilities
- Click ”+ Capability”
- Add “Maps”
- Check the routing modes your app supports (Transit, Automobile, Pedestrian, Cycling)
This adds the com.apple.developer.maps entitlement to your app.
Info.plist Configuration
Add the routing mode declarations to your Info.plist:
<key>MKDirectionsApplicationSupportedModes</key>
<array>
<string>MKDirectionsModesTransit</string>
<string>MKDirectionsModesAutomobile</string>
</array>
Only declare modes your app actually supports. If you declare transit mode but your app doesn’t provide transit directions, Apple will reject your submission.
URL Scheme for Routing Requests
Your app receives routing requests through a URL scheme. Register the directions URL type in your Info.plist:
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>MKDirectionsRequest</string>
<key>LSItemContentTypes</key>
<array>
<string>com.apple.maps.directionsrequest</string>
</array>
</dict>
</array>
This tells iOS that your app can handle MKDirectionsRequest objects.
Project Structure
Organize your project to separate routing logic from the rest of your app:
YourApp/
Routing/
RoutingRequestHandler.swift - Parses incoming MKDirectionsRequest
DirectionsViewController.swift - Displays route to the user
CoverageManager.swift - Manages coverage area data
Resources/
coverage.geojson - Your routing coverage file
Step 3: Build the GeoJSON Coverage File
The coverage file defines the geographic boundaries where your app can provide directions. Precision matters - an area too small means you miss users who could use your app, an area too large means users select your app for routes you can’t handle.
GeoJSON Structure
A minimal coverage file with a single coverage area:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "Dublin Metro Area",
"modes": ["MKDirectionsModesTransit"]
},
"geometry": {
"type": "Polygon",
"coordinates": [[
[-6.4, 53.2],
[-6.0, 53.2],
[-6.0, 53.45],
[-6.4, 53.45],
[-6.4, 53.2]
]]
}
}
]
}
Coordinate Format
GeoJSON uses longitude-latitude order (not latitude-longitude, which is the opposite of what most people expect):
[longitude, latitude]
[-6.2603, 53.3498] // Dublin city center
This is the single most common mistake in coverage files. If your polygons appear on the wrong side of the world when you visualize them, you have the coordinates swapped.
Polygon Construction
Simple rectangle: For a city or metropolitan area, a bounding rectangle is the easiest starting point:
{
"type": "Polygon",
"coordinates": [[
[-6.45, 53.20],
[-6.05, 53.20],
[-6.05, 53.45],
[-6.45, 53.45],
[-6.45, 53.20]
]]
}
The first and last coordinate must be identical to close the ring.
Detailed boundary: For precise coverage that follows city limits, transit network boundaries, or service area borders, use more coordinate points:
{
"type": "Polygon",
"coordinates": [[
[-6.385, 53.285],
[-6.350, 53.270],
[-6.280, 53.260],
[-6.200, 53.265],
[-6.140, 53.290],
[-6.100, 53.320],
[-6.095, 53.360],
[-6.110, 53.400],
[-6.170, 53.420],
[-6.260, 53.430],
[-6.340, 53.415],
[-6.390, 53.380],
[-6.400, 53.340],
[-6.385, 53.285]
]]
}
MultiPolygon for non-contiguous areas: If your app covers multiple separate regions, use a MultiPolygon or multiple features:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "Dublin",
"modes": ["MKDirectionsModesTransit"]
},
"geometry": {
"type": "Polygon",
"coordinates": [[ ... ]]
}
},
{
"type": "Feature",
"properties": {
"name": "Cork",
"modes": ["MKDirectionsModesTransit"]
},
"geometry": {
"type": "Polygon",
"coordinates": [[ ... ]]
}
}
]
}
Generating Coverage Boundaries
For transit apps, your coverage area should match your transit data coverage. Here are approaches for generating accurate boundaries:
From GTFS data: If you use GTFS (General Transit Feed Specification) data, extract the convex hull or bounding area of all stop locations:
import json
from shapely.geometry import MultiPoint, mapping
# Load stop coordinates from GTFS stops.txt
stops = [(stop_lon, stop_lat) for stop_lon, stop_lat in stop_data]
# Create a convex hull with a small buffer
coverage = MultiPoint(stops).convex_hull.buffer(0.01)
# Convert to GeoJSON
geojson = {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"properties": {"name": "Transit Coverage", "modes": ["MKDirectionsModesTransit"]},
"geometry": mapping(coverage)
}]
}
with open("coverage.geojson", "w") as f:
json.dump(geojson, f)
From administrative boundaries: Use open data sources like OpenStreetMap, Natural Earth, or government boundary datasets to get precise city or region boundaries. Simplify complex boundaries to reduce file size while maintaining accuracy.
Manual drawing: Use geojson.io, Google Earth, or QGIS to manually draw your coverage area on a map and export as GeoJSON. This is practical for small coverage areas or when you need human judgment to define boundaries.
Coverage Area Sizing Strategy
Start slightly larger than your actual coverage. If your transit data covers routes within Dublin city limits, extend your coverage polygon 5-10 km beyond the city boundary. This ensures that users requesting routes from suburban areas just outside the city still see your app as an option.
Don’t cover areas you can’t serve. If a user selects your app for a route outside your actual service area and your app can’t provide directions, the user experience is terrible. They have to go back to Maps and choose another option. This hurts your app’s reputation and may lead to negative reviews.
Step 4: Validate the GeoJSON File
A malformed GeoJSON file will be rejected by App Store Connect or cause unpredictable behavior in Apple Maps. Validate thoroughly before submitting.
Structural Validation
Run your file through a GeoJSON validator:
Online tools:
- geojsonlint.com - validates structure and geometry
- geojson.io - validates and visualizes simultaneously
Command-line tools:
# Using Python with geojson library
python -c "
import json
with open('coverage.geojson') as f:
data = json.load(f)
print('Type:', data['type'])
for i, feature in enumerate(data['features']):
geom = feature['geometry']
print(f'Feature {i}: {geom[\"type\"]} with {len(geom[\"coordinates\"])} rings')
"
Using ogr2ogr (GDAL):
ogrinfo -al coverage.geojson
Common Validation Errors
| Error | Cause | Fix |
|---|---|---|
| Invalid JSON | Syntax error (missing comma, bracket, quote) | Run through a JSON linter first |
| Self-intersecting polygon | Polygon edges cross each other | Simplify the polygon or fix the crossing points |
| Unclosed ring | First and last coordinates don’t match | Copy the first coordinate to the end of the ring |
| Wrong coordinate order | Latitude-longitude instead of longitude-latitude | Swap all coordinate pairs |
| Clockwise exterior ring | Exterior rings should be counterclockwise per RFC 7946 | Reverse the coordinate order |
| Too many decimal places | Coordinates with 15 decimal places bloat the file | Round to 6 decimal places (11 cm precision) |
| Empty geometry | Feature has no coordinates | Remove the empty feature or add coordinates |
Visual Verification
Always render your coverage file on a map before submitting:
- Open geojson.io in a browser
- Paste your GeoJSON content or drag the file onto the map
- Verify the polygons cover the correct geographic areas
- Check for gaps between adjacent polygons (if covering multiple connected areas)
- Verify the polygons don’t extend into areas you can’t serve (oceans, countries without coverage)
Size Optimization
If your coverage file exceeds 5 MB:
- Reduce coordinate precision to 5-6 decimal places
- Simplify polygon boundaries (fewer points for the same shape)
- Use tools like mapshaper.org to reduce vertex count while preserving shape
- Remove unnecessary properties from feature objects
Apple accepts files up to 20 MB, but smaller files process faster and are less likely to encounter submission issues.
Step 5: Test Routing Integration Locally
Before submitting to App Store Connect, test the entire flow locally to confirm your app correctly receives and handles routing requests.
Simulator Configuration
Set a custom location in the iOS Simulator that falls within your coverage area:
- Run your app in the Simulator
- Go to Features > Location > Custom Location
- Enter coordinates inside your coverage polygon (e.g., 53.3498, -6.2603 for Dublin)
Testing with Maps
With your app installed on the Simulator:
- Open Apple Maps
- Search for a destination within your coverage area
- Tap “Directions”
- Your app should appear as a routing option if:
- The route endpoints fall within your coverage polygon
- Your app declares the correct routing mode for that area
- Your Info.plist configuration is correct
If your app doesn’t appear, check:
- The simulator location is within your coverage polygon
- The routing mode you’re testing matches what your Info.plist declares
- Your app’s Maps capability is correctly configured
- The coverage file is included in your app bundle (for local testing)
Testing on a Physical Device
For more realistic testing, install your development build on a physical device:
- Open Maps while physically located in your coverage area (or use a GPX file to simulate the location)
- Request directions between two points in your coverage area
- Verify your app appears as an option
- Select your app and verify the routing request is received correctly
MKDirectionsRequest Debugging
Add logging to your routing request handler to verify the data you receive:
func application(_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
guard let request = MKDirectionsRequest(contentsOf: url) else {
print("[Routing] Failed to parse MKDirectionsRequest from URL: \(url)")
return false
}
if let source = request.source?.placemark.coordinate {
print("[Routing] Source: \(source.latitude), \(source.longitude)")
}
if let dest = request.destination?.placemark.coordinate {
print("[Routing] Destination: \(dest.latitude), \(dest.longitude)")
}
print("[Routing] Transport type: \(request.transportType.rawValue)")
print("[Routing] Departure date: \(String(describing: request.departureDate))")
print("[Routing] Arrival date: \(String(describing: request.arrivalDate))")
return true
}
Step 6: Submit Through App Store Connect
Once testing confirms everything works, submit your coverage file alongside your app binary.
Upload Process
In App Store Connect:
- Navigate to your app > App Information
- Scroll to the “Routing App Coverage File” section
- Upload your
.geojsonfile - Verify the upload shows the correct file name and size
The coverage file is associated with your app, not a specific version. You can update it independently of app updates, though changes still go through Apple’s review process.
Submission Checklist
Before submitting, verify:
- GeoJSON file passes structural validation
- Polygons cover your actual service area (not more, not less)
- Coordinate order is longitude-latitude
- All polygon rings are closed
- Properties include correct routing mode identifiers
- File size is under 20 MB
- Info.plist declares matching routing modes
- Maps capability is enabled in your app target
- URL scheme handles MKDirectionsRequest
- Local testing confirms routing requests work
- App correctly displays directions for received requests
Review Expectations
Apple reviews coverage files as part of the app review process. Common rejection reasons:
- Coverage area doesn’t match app functionality: You claim to cover an area but your app can’t actually provide directions there
- Incorrect routing modes: Your Info.plist declares transit mode but your app only provides driving directions
- Malformed GeoJSON: The file has structural errors that slipped past your validation
- Excessive coverage: Claiming global coverage when your app only works in specific cities
If rejected, Apple’s review notes will explain the issue. Fix the specific problem and resubmit.
Step 7: Handle Incoming Routing Requests
When a user selects your app in Apple Maps, iOS sends a routing request to your app. Your code needs to parse this request and display relevant directions.
Parsing the Request
Implement the URL handler in your AppDelegate or SceneDelegate:
func application(_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
guard let request = MKDirectionsRequest(contentsOf: url) else {
return false
}
let routingHandler = RoutingRequestHandler()
routingHandler.handleRequest(request)
return true
}
Extracting Route Parameters
The MKDirectionsRequest object contains:
class RoutingRequestHandler {
func handleRequest(_ request: MKDirectionsRequest) {
// Source location (where the user is or wants to start)
let source = request.source?.placemark.coordinate
let sourceName = request.source?.placemark.name
// Destination location
let destination = request.destination?.placemark.coordinate
let destinationName = request.destination?.placemark.name
// Transport type
let mode = request.transportType
// .automobile, .transit, .walking, .cycling
// Time preferences
let departureDate = request.departureDate
let arrivalDate = request.arrivalDate
// Route the user to your directions screen
showDirections(
from: source,
to: destination,
mode: mode,
departAt: departureDate,
arriveBy: arrivalDate
)
}
func showDirections(from source: CLLocationCoordinate2D?,
to destination: CLLocationCoordinate2D?,
mode: MKDirectionsTransportType,
departAt: Date?,
arriveBy: Date?) {
// Navigate to your directions view controller
// with the provided parameters
}
}
Handling Edge Cases
Missing source location: The source may be nil if the user didn’t specify a starting point. Default to the user’s current location:
let startCoordinate = source ?? locationManager.location?.coordinate
Missing destination: Rare but possible. Show your app’s default search screen so the user can enter a destination.
Unsupported route: If the requested route falls partially outside your coverage (e.g., the source is in your area but the destination is not), handle gracefully:
if !coverageArea.contains(destination) {
showErrorMessage("We don't have coverage for this destination yet. Please try another routing app.")
return
}
App launched from background:
If your app was in the background when the routing request came in, handle the request in both application(_:open:options:) and scene(_:openURLContexts:) to cover both launch and resume scenarios.
Deep Linking from Routing Requests
Track routing requests as a discovery channel in your analytics:
Analytics.log(event: "routing_request_received", properties: [
"source": "apple_maps",
"transport_type": mode.description,
"has_departure_time": departureDate != nil,
"has_arrival_time": arrivalDate != nil
])
This data helps you understand how much traffic Apple Maps drives to your app and whether those users convert to regular usage.
Step 8: Monitor and Expand Coverage
After launch, track how your routing integration performs and plan coverage expansion.
Performance Metrics
Track these metrics to understand the value of your Apple Maps integration:
- Routing requests received: How many times per day/week Apple Maps sends users to your app
- Request completion rate: How many routing requests result in the user actually following the directions
- Session duration from routing: How long do users who come from Maps stay in your app
- Install attribution: If possible, measure how many users discover your app because it appeared in Maps
- Coverage utilization: Which parts of your coverage area generate the most routing requests
Expanding to New Areas
When your app adds coverage for new cities or regions:
- Update your GeoJSON file with new polygon features
- Test the new boundaries locally
- Upload the updated file to App Store Connect
- Submit for review (coverage file updates don’t require a new app version, but they do require review)
Plan coverage expansion based on:
- User demand: Cities where users request your app most (check support tickets, app reviews, social media)
- Transit data availability: Cities where you have reliable transit data (GTFS feeds, real-time API access)
- Competitive landscape: Cities where no other routing app provides good coverage (use ASODOG to analyze which transit apps are popular in different regions)
- Market size: Cities with high public transit ridership and smartphone penetration
Keeping Coverage Current
Transit networks change. Bus routes are added, train lines are extended, service areas grow. Update your coverage file whenever:
- Your transit data adds new routes that extend beyond your current coverage polygon
- You discontinue service in an area (shrink the polygon)
- Administrative boundaries change (city mergers, new transit authority service areas)
- You improve routing accuracy in areas that were previously too unreliable to include
Set a quarterly reminder to review your coverage boundaries against your actual routing data. A coverage file that is out of sync with your app’s capabilities leads to either missed users (coverage too small) or frustrated users (coverage too large).
ASO Impact of Routing Integration
Your Apple Maps routing integration affects your overall App Store Optimization in several ways:
- Discovery channel: Users who find your app through Maps are high-intent organic users, contributing to your download velocity
- Engagement signals: Routing sessions count as active app usage, improving your engagement metrics
- Retention boost: Users who use your app for daily commute routing tend to have higher retention than users from other channels
- Keyword relevance: Routing activity reinforces your app’s relevance for transit and navigation keywords in App Store search
- Review quality: Users who find genuine utility in your routing tend to leave detailed positive reviews
Use ASODOG to correlate coverage expansion milestones with changes in your keyword rankings, category position, and download trends. This data helps you quantify the ASO value of investing in broader geographic coverage.