iPhoneアプリでGoogleの逆ジオコーディングを使ってMKPlacemarkを構築するには?(日本国内版)

iPhoneアプリで逆ジオコーディングを行うMKReverseGeocoderには、PBRequesterErrorDomainエラーで突然逆ジオコーディングができなくなる問題があることがわかった。

琴線探査: iPhoneの逆ジオコーディングでPBRequesterErrorDomainエラー。何故?

そこで、MKRverseGeocoderのfailover用として「The Google Geocoding Web Service」を使って逆ジオコーディングする方法を試した。
The Google Geocoding Web Service - Google Maps API Web Services - Google Code

結果は上々。逆ジオコーディングの精度も高く、MKReverseGeocoderで取得できない情報もその気になれば取得できることがわかった。ただ問題は、Googleの逆ジオコーディングの結果から、いかにMKPlacemarkを構築するか、ということだった。

アプリケーションによっては一つの文字列になっている「Formatted Address」さえあれば事足りる場合もあるけど、今回はGoogleの逆ジオコーディングの結果を解析して、国、県、郡、区、市、村、町・字、街区、地番の各レベルごとに情報を認識する必要があった。

住所の扱いは非常に複雑で、とりあえず日本国内だけサポートするのが精一杯だったのだけど、例えばこのようにすれば可能だ。

/**
 *
 * Googleの逆ジオコーディング開始
 *
 */
- (void)startReverseGeocoderGoogle:(CLLocationCoordinate2D)coordinate {

  //逆ジオコーディング成功フラグ
  BOOL isRGeoOK = NO;
 
  //アプリケーション取得
  UIApplication *app = [UIApplication sharedApplication];

  //ネットワークインジケーターON
  app.networkActivityIndicatorVisible = YES;

  //URL構築
  NSString *urlStr = [NSString stringWithFormat:@"http://maps.google.com/maps/api/geocode/json?latlng=%f,%f&sensor=false", coordinate.latitude, coordinate.longitude];
  NSURL *url = [NSURL URLWithString:urlStr];

  //逆ジオコーディング結果の文字列(JSON)
  NSString *rgeoRetStr = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:nil];
  if (rgeoRetStr != nil) {
  
    NSLog(@"Googleの逆ジオコーディングレスポンス\n%@", rgeoRetStr);

    //逆ジオコーディング結果の文字列をJSONとしてパースしてルートディクショナリを構築
    NSDictionary* dict = [rgeoRetStr JSONValue];
  
    //ステータスを取得
    NSString *status = [dict objectForKey:@"status"];
  
    //ステータスを判断
    if ([status isEqualToString:@"OK"]) {
   
      //結果は幾つもある場合がある
      NSArray *results = [dict objectForKey:@"results"];

      //大抵信用出来る情報は配列の頭に来る
      NSDictionary *result = [results objectAtIndex:0];

      //MKPlacemark用の空addressDictionaryを作る
      NSMutableDictionary *addrDict = [NSMutableDictionary dictionaryWithCapacity:7];

      //フォーマットされた住所文字列を取得してaddressDictionaryに追加
      NSString *formattedAddress = [result objectForKey:@"formatted_address"];
      NSLog(@"formatted_address:%@", formattedAddress);
      [addrDict setObject:formattedAddress forKey:@"FormattedAddress"];
     
      //FormattedAddressLinesをaddressDictionaryに追加
      NSArray *formattedAddresLines = [NSArray arrayWithObject:formattedAddress];
      [addrDict setObject:formattedAddresLines forKey:@"FormattedAddressLines"];
  
      //Thoroughfare判断用全角数字配列(ループで作成しまくらないようにループ外で作成)
      NSArray *nums = [NSArray arrayWithObjects:@"0",@"1",@"2",@"3",@"4",@"5",@"6",@"7",@"8",@"9", nil];
   
      //フォーマットされた住所文字列の中身を区切ったものが入っている配列
      //「東京都, 日本」のように大きい区分が後ろに来るので逆からスキャンする
      NSArray *addressComponents = [result objectForKey:@"address_components"];
      for (int i = addressComponents.count - 1; i >= 0; i--) {

        //「addressComponent」には、「long_name」「short_name」「types」が入っている
        NSDictionary *addressComponent = [addressComponents objectAtIndex:i];
        NSString *longName = [addressComponent objectForKey:@"long_name"];

        //「types」には文字列の属性(「country」や「political」)が含まれる
        //必要なのは先頭の文字列(countryなど)
        NSArray *types = [addressComponent objectForKey:@"types"];
        NSString *type = [types objectAtIndex:0];

        //「type」属性で判断してMKPlacemark用のaddressDictionaryを構築していく
        NSLog(@"addressComponent longName=%@, type=%@", longName, type);

        //「country」には国名(日本など)と国コード(JPなど)が含まれる
        if ([type isEqualToString:@"country"]) {
          [addrDict setObject:longName forKey:@"Country"];
          NSString *shortName = [addressComponent objectForKey:@"short_name"];
          [addrDict setObject:shortName forKey:@"CountryCode"];
        }
  
        //「administrative_area_level_1」には都道府県が含まれる
        if ([type isEqualToString:@"administrative_area_level_1"]) {
          [addrDict setObject:longName forKey:@"State"];
        }

        //「locality」には郡、区、市町村レベルが含まれる
        if ([type isEqualToString:@"locality"]) {

          //郡はSubAdministrativeAreaにマッピング
          if ([longName hasSuffix:@"郡"]) {
            [addrDict setObject:longName forKey:@"SubAdministrativeArea"];
          }

          //区
          else if ([longName hasSuffix:@"区"]) {

            //東京都でない場合(政令指定都市)はkuにマッピング
            //(「ku」はMKPlacemarkのプロパティーにはない独自のキー)
            NSRange searchResult = [formattedAddress rangeOfString:@"東京都"];
            if (searchResult.location == NSNotFound) {
              [addrDict setObject:longName forKey:@"ku"];
            }

            //東京都の場合はCityにマッピング
            else {
              [addrDict setObject:longName forKey:@"City"];
            }
      
          }

          //市町村レベル
          else {
            [addrDict setObject:longName forKey:@"City"];
          }
     
        }
    
        //「sublocality」には町・字レベル(〜町)、街区レベル(〜丁目)、地番レベル(7-3)が含まれる
        if ([type isEqualToString:@"sublocality"]) {

          //全角数字で終わっているかどうか
          BOOL isEndWithNum = NO;
          for (id str in nums) {
            if ([longName hasSuffix:str]) {
              isEndWithNum = YES;
              break;
          }

          //全角数字で終わっているならSubThoroughfareとみなす
          if (isEndWithNum) {

            //地番レベル
            if (![addrDict valueForKey:@"SubThoroughfare"]) {
              [addrDict setObject:longName forKey:@"SubThoroughfare"];
            }

            //地番レベルがすでに存在するなら「-」で連結する
            else {
              NSString *subThroughfare = [[addrDict valueForKey:@"SubThoroughfare"] stringByAppendingString:@"-"];
              subThroughfare = [subThroughfare stringByAppendingString:longName];
              [addrDict setObject:subThroughfare forKey:@"SubThoroughfare"];
            }
      
          } else {

            //町・字レベル(〜町)
            if (![addrDict valueForKey:@"SubLocality"]) {
              [addrDict setObject:longName forKey:@"SubLocality"];
            }

            //街区レベル(〜丁目)
            //全角数字で終わっていなければThoroughfareとみなす
            //〜丁目で終わらない場合もある
            //(徳島県鳴門市撫養町南浜東浜〜、岩手県花巻市高松第8地割〜など)
            else if (![addrDict valueForKey:@"Thoroughfare"]) {
              [addrDict setObject:longName forKey:@"Thoroughfare"];
            }
      
          }

        }

      }

      //キーと値を列挙(デバッグ用)
      for (id key in addrDict) {
        NSLog(@"addrDict key=%@, value=%@", key, [addrDict valueForKey:key]);
      }

      //MKPlacemark構築
      MKPlacemark *pmark = [[MKPlacemark alloc] initWithCoordinate:coordinate addressDictionary:addrDict];

      // ---------- ここでMKPlacemarkを使って何かをする ----------
      
      //MKPlacemark解放
      [pmark release], pmark = nil;

      //逆ジオコーディング成功フラグを立てる
      isRGeoOK = YES;
      NSLog(@"Googleの逆ジオコーディング成功!");

    } else {
      NSLog(@"Googleの逆ジオコーディング失敗 status:%@", status);
    }

  } else {
    NSLog(@"Googleの逆ジオコーディング失敗:レスポンスなし");
  }
 
  //逆ジオコーディングが失敗したら
  if (!isRGeoOK) {
    //何かする
  }

  //ネットワークインジケーターOFF
  app.networkActivityIndicatorVisible = NO;

}

このコードをコンパイルするには、特にStig Brautaset氏の「json-framework」が必要だ。
琴線探査: iPhoneアプリでJSONを簡単に扱うには?「json-framework」 by Stig Brautaset

プログラム的に日本国内の数百地点をランダムにテストし、おかしな解析をしていないか調べたけど、今のところ見つかっていない。

日本の住所の大まかなパターンは前に調べたので最低でもこれらのパターンは網羅したつもり。
琴線探査: 日本の住所のパターンは一体いくつあるか?

しかし、細かい部分でまだ思いもよらない住所パターンがあるかもしれない。

------

もしこのコードがiPhone開発者の同志の皆様の役に立つのであれば嬉しいのですが、責任は一切持てませんので、よろしくお願いします。

そして、もしこのコードを使っておかしな解析結果を見つけた方は、是非コメントでお知らせいただけると助かります。その他にも改善点などをご指摘いただければ、これも助かります。よろしくお願いします。

最後に、この素晴らしい「json-framework」の作者のStig Brautaset氏に敬礼!
Thank you very much, Stig!

コメント

このブログの人気の投稿

レオナルド・ダ・ビンチはなぜノートを「鏡文字」で書いたのか?

macでsmb(samba)共有サーバーに別名で接続(別アカウント名で接続)する方法

Google DriveにCURLでアップロードするには?