iPad/iPhone forward geocoding API using Google gecoding service

The iPhone (and now iPad) SDK supports reverse geocoding out of the box. Reverse geocoding means you have a coordinate and want to find out where you are located, for instance, the name of the street you are on. Forward geocoding means you know the name of a location and want to find the coordinates. For reasons not totally clear to me there’s no support for forward geocoding, I’ve read somewhere that it has something to do with license deals.
I’ve started working a little with the iPad simulator and with its large display it makes sense presenting information on a map view. Because I haven’t found a good example/API for forward geocoding I’ve decided to publish my own. There are a few players offering geocoding services, Yahoo, CloudeMade, Tele Atlas and of course Google. Yahoo and Google are both free but I’ve decided on using Google.

This sample iPad application contains a search bar and a large UIMapView. Search results are visualized on the map with a placemark and by clicking the placemark the map will zoom to the viewport returned from the geocoding service. When I started working with the geocoding service it was still in version two, this last week Google launched version three. Version two will be depricated of course but I had already written the parser for version two so I’ve included it as well in my sample. Of course the API works for iPhone as well. Because the API contains quite a lot of code you will find the code inside the sample project at the bottom of this page.

Forward geocoding APi for iPad and iPhone

Using my Forward geocoding API

Using BSForwardGeocoder is pretty straight forward. Example below:

- (void) searchBarSearchButtonClicked:(UISearchBar *)theSearchBar {
 
	NSLog(@"Searching for: %@", searchBar.text);
	if(forwardGeocoder == nil)
	{
		forwardGeocoder = [[BSForwardGeocoder alloc] initWithDelegate:self];
	}
 
	// Forward geocode!
	[forwardGeocoder findLocation:searchBar.text];
 
}
-(void)forwardGeocoderFoundLocation
{
	if(forwardGeocoder.status == G_GEO_SUCCESS)
	{
		int searchResults = [forwardGeocoder.results count];
 
		// Add placemarks for each result
		for(int i = 0; i < searchResults; i++)
		{
			BSKmlResult *place = [forwardGeocoder.results objectAtIndex:i];
 
			// Add a placemark on the map
			CustomPlacemark *placemark = [[CustomPlacemark alloc] initWithRegion:place.coordinateRegion];
			placemark.title = place.address;
			[mapView addAnnotation:placemark];	
 
			NSArray *countryName = [place findAddressComponent:@"country"];
			if([countryName count] > 0)
			{
				NSLog(@"Country: %@", ((BSAddressComponent*)[countryName objectAtIndex:0]).longName );
			}
 
			[countryName release];
		}
 
		if([forwardGeocoder.results count] == 1)
		{
			BSKmlResult *place = [forwardGeocoder.results objectAtIndex:0];
 
			// Zoom into the location		
			[mapView setRegion:place.coordinateRegion animated:TRUE];
		}
 
		// Dismiss the keyboard
		[searchBar resignFirstResponder];
	}
}

When search has executed the geocoder contains the result and a status code. The status code is from Google and to support both version 2 and 3 of the API there’s an enum containing response codes. If everything goes well the status should be “G_GEO_SUCCESS” and the “results” property will contain an array of BSKmlResult objects which contain the location information returned for the query. Here’s an example query for my home town Stockholm (Google geocoding service version 3) : http://maps.google.com/maps/api/geocode/xml?address=stockholm&sensor=false.
The details for the service can be found here: http://code.google.com/apis/maps/documentation/geocoding/.
There are multiple arguments you can pass to the search url, you should read the geocoding documentation and check the search url in the application before you implement this in your own app.

BSForwardGeocoder

#import <Foundation/Foundation.h>
#import "BSGoogleV2KmlParser.h"
#import "BSGoogleV3KmlParser.h"
 
// Enum for geocoding status responses
enum {
	G_GEO_SUCCESS = 200,
	G_GEO_BAD_REQUEST = 400,
	G_GEO_SERVER_ERROR = 500,
	G_GEO_MISSING_QUERY = 601,
	G_GEO_UNKNOWN_ADDRESS = 602,
	G_GEO_UNAVAILABLE_ADDRESS = 603,
	G_GEO_UNKNOWN_DIRECTIONS = 604,
	G_GEO_BAD_KEY = 610,
	G_GEO_TOO_MANY_QUERIES = 620	
};
 
@protocol BSForwardGeocoderDelegate <NSObject>
@required
-(void)forwardGeocoderFoundLocation;
@optional
-(void)forwardGeocoderError:(NSString *)errorMessage;
@end
 
@interface BSForwardGeocoder : NSObject {
	NSString *searchQuery;
	NSString *googleAPiKey;
	int status;
	NSArray *results;
	id<BSForwardGeocoderDelegate> delegate;
}
-(id) initWithDelegate:(id<BSForwardGeocoderDelegate>)del;
-(void) findLocation:(NSString *)searchString;
 
@property (assign) id<BSForwardGeocoderDelegate> delegate;
@property (nonatomic, retain) NSString *searchQuery;
@property (nonatomic, readonly) int status;
@property (nonatomic, retain) NSArray *results;
 
@end

BSKmlResult
The result class is the same for both version 2 and 3 of the service. A big difference in the returned information between the versions is the way address components are returned, in version three more information is returned and therefore I’ve created another class to store the address component information. The properties: countryNameCode, countryName, subAdministrativeAreaName and localityName are for version two only. For version three all address information is stored in the “addressComponents” array (contains BSAddressComponent objects). Because there is really no reason to use version 2 anymore you probably want to remove this code for your own application.

#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
#import "BSAddressComponent.h"
 
@interface BSKmlResult : NSObject {
	NSString *address;
	NSString *countryNameCode;
	NSString *countryName;
	NSString *subAdministrativeAreaName;
	NSString *localityName;
	float viewportSouthWestLat;
	float viewportSouthWestLon;
	float viewportNorthEastLat;
	float viewportNorthEastLon;
	float boundsSouthWestLat;
	float boundsSouthWestLon;
	float boundsNorthEastLat;
	float boundsNorthEastLon;
	float latitude;
	float longitude;
	float height;
	NSInteger accuracy;
	NSArray *addressComponents;
}
 
@property (nonatomic, retain) NSString *address;
@property (nonatomic, assign) NSInteger accuracy;
@property (nonatomic, retain) NSString *countryNameCode;
@property (nonatomic, retain) NSString *countryName;
@property (nonatomic, retain) NSString *subAdministrativeAreaName;
@property (nonatomic, retain) NSString *localityName;
@property (nonatomic, retain) NSArray *addressComponents;
@property (nonatomic, assign) float latitude;
@property (nonatomic, assign) float longitude;
@property (nonatomic, assign) float viewportSouthWestLat;
@property (nonatomic, assign) float viewportSouthWestLon;
@property (nonatomic, assign) float viewportNorthEastLat;
@property (nonatomic, assign) float viewportNorthEastLon;
@property (nonatomic, assign) float boundsSouthWestLat;
@property (nonatomic, assign) float boundsSouthWestLon;
@property (nonatomic, assign) float boundsNorthEastLat;
@property (nonatomic, assign) float boundsNorthEastLon;
@property (readonly) CLLocationCoordinate2D coordinate;
@property (readonly) MKCoordinateSpan coordinateSpan;
@property (readonly) MKCoordinateRegion coordinateRegion;
 
-(NSArray*)findAddressComponent:(NSString*)typeName;
 
@end

To make it somewhat simple to find address components I’ve added a method that will search for components for you. Using version 3 of the geocoding service you will get the country name using this code:

NSArray *countryName = [BSKmlResultPlace findAddressComponent:@"country"];
if([countryName count] > 0)
{
	NSLog(@"Country: %@", ((BSAddressComponent*)[countryName objectAtIndex:0]).longName );
}
[countryName release];

There are also properties to make the result simple to use with a MKMapView. The “coordinate” property returns a CLLocationCoordinate2D object, “coordinateSpan” calculates and returns a MKCoordinateSpan object for setting the map viewport. The “coordinateRegion” combines both coordinate and coordinateSpan returning a MKCoordinateRegion object that can be used to directly move your MKMapView to the right place.

BSAddressComponent

#import <Foundation/Foundation.h>
 
 
@interface BSAddressComponent : NSObject {
	NSString *longName;
	NSString *shortName;
	NSArray *types;
}
 
@property (nonatomic, retain) NSString *longName;
@property (nonatomic, retain) NSString *shortName;
@property (nonatomic, retain) NSArray *types;

This class maps against what’s returned in version 3 of the geocoding service. It’s just a container class.

Sample code

You will find the API inside this sample project. The code has now moved to GitHub, fork away!

BSForwardGeocoder @ GitHub

Happy geocoding!

103 thoughts on “iPad/iPhone forward geocoding API using Google gecoding service

  1. Thank you for your awesome code. I tried to add component filtering based on the Google Geocode API to my search URL and I keep getting the message “bad URL”. This is what the URL looks like as it is being sent to Google for geocoding. Any idea what could be “bad” in this:
    https://maps.google.com/maps/api/geocode/xml?address=Da%20GEORGIA&components=administrative_area:Da|country:US&sensor=false&bounds=34.172684%2C-118.604794%7C34.236144%2C-118.500938

    Thanks again for your excellent code.

    Paul

  2. Okay I figured it the solution to my question. In my search URL, I have to encode the pipe character with this: “%7C”. Now it works and I have component filtering on top of your primo code.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>