読者です 読者をやめる 読者になる 読者になる

eps_r

不思議の謎を解かねばならぬ

ボーダーブレイク 店舗マップとGoogle Geocoding APIについて

ボーダーブレイク店舗情報ウォッチbotとGoogle Apps Scriptについてtwitter botの話をしたので、今回はそれに連動しているボーダーブレイク 店舗マップという物について簡単な説明をします。

最初に

こういうのがGoogle Mapsで表示されます。表示を秋葉原周辺に拡大していますが、マーカーは全国に刺さっています。

大きい地図はこちら。

マーカーの配置についての情報はひとつのxmlファイルに記述されており、サーバーに置かれたこれをGoogle Mapsに読み込ませることで表示を実現しています。

今回はすでに存在する名前-住所の組に対し、経度緯度を集めてxmlファイル(拡張子kml)を出力するまでの一連の仕組みについて少し書きます。

プラットフォーム

前回に加えて、住所→経度緯度変換のためのGeocoding API、ファイル出力のためのDrive、表示のためにマップを使います。

Google Geocoding APIは、住所から経度緯度の変換を行うジオコーディング、経度緯度から住所への変換を行う逆ジオコーディングの機能を提供するAPIです。v3になってからはAPIキーの申請なども必要なく規約に則って気軽に使えます。

構成

  • スプレッドシート
    • 店舗情報シート(店舗情報ウォッチbotで使っている「今日の店舗情報シート」と同じ、毎日21時頃更新される)
    • ジオコードキュー兼キャッシュのシート
  • Google Drive
    • KMLファイル
  • Apps Scriptの定期実行ジョブ(トリガー)
    • (毎日22時頃)ジオコードキューを生成
    • (5分ごと)ジオコーディング実行
    • (12時間ごと)KMLファイル出力

Google Driveとマップの連携が上手くいかなかったため最終出力のkmlファイルはDropboxと同期していますが、これは本筋ではないのでまた別の機会に書きます。

処理の流れ

(毎日実行)ジオコードキュー生成

  1. 店舗情報シートとジオコードキャッシュを比較し、店舗名か住所が未知のものであればジオコードキャッシュに店舗名・住所を追記

店舗情報シートは前回書いた店舗名と住所を格納した表(公式サイトから取得)です。ジオコードキャッシュは下記のようになります。

店舗名 住所 作成日時 更新日時 APIの返す住所 緯度 経度 住所タイプ 部分マッチ API発行回数
ゲーム○○ XX県XX市XX町1-1-1 YYYY/MM/DD hh:mm:ss

(5分ごとに実行)ジオコーディング

  1. ジオコードキャッシュに経度緯度の未入力になっている行があればジオコーディングを実行し、キャッシュの該当行に経度緯度を追記

更新日時より右をジオコーディング実行時刻とAPIのレスポンスで埋めます。

店舗名 住所 作成日時 更新日時 APIの返す住所 緯度 経度 住所タイプ 部分マッチ API発行回数
ゲーム○○ XX県XX市XX町1-1-1 YYYY/MM/DD hh:mm:ss YYYY/MM/DD hh:mm:ss XX県XX市XX町1-1-1 yyy.yyy xxx.xxx sublocality_level_4, sublocality, political false 1

住所タイプ(types)は応答に付与されるタグの配列です。politicalは政治的な分類であること(特定のランドマークや空港などでなく地域であるという位の意味)、sublocalityは町や村などの下位分類にあること、sublocality_level_4は分類の深さを示します。

部分マッチ(partical_match)は住所との部分的一致を示すbool値です。trueのときに意図と違う住所を指し示す場合もあるので、後で確認できるようレコードに残しています。

API発行回数は、返ってくる経度緯度の精度を補助するために住所を加工して何度かリクエストを行うので、結果を得るために掛かった回数を記録しています。

(毎日2回実行)KMLファイルを出力

  1. マスタとジオコードキャッシュを参照してKMLファイルを作成・上書き保存する

(スクリプト外の作業)Google Mapsへのリンクを作る

先述の通り出来上がったKMLファイルはDropboxと同期して置いています。

下のようなURLを作成しリンクを貼れば、検索窓にkmlファイルを放り込む格好になります。

https://maps.google.co.jp/maps?q=http:%2F%2Fdl.dropboxusercontent.com%2Fu%2F107335829%2Fxml%2Fborderbreak-satellites.kml

2014年9月現在いつまでプレビューやってんだという感のあるPC版Google Mapsの新バージョンはKMLファイルリーダを実装していませんが、KMLファイルのURLが入力されると自動的に旧いバージョンに遷移するのでとりあえず閲覧には問題ないことになっています。

サンプルコード

ジオコーディング(住所→経度緯度変換)

Geocoding APIを呼ぶ方法として、Google App Script内ではGeocoder Classが用意されています。

function geocode(address) {
  var response = Maps.newGeocoder().setRegion("jp").setLanguage("ja").geocode("千代田区丸の内 2-4-1");
  for (var i = 0; i < response.results.length; i++) {
    var result = response.results[i];
    Logger.log('%s: %s, %s: %s', result.formatted_address, result.geometry.location.lat,
               result.geometry.location.lng, result.types);
  }
}

以下のようなログが出ます。緯度経度が取得され、省いておいた国名・都道府県がAPIで補われているのが分かります。

# APIの返す地名、緯度、経度、住所タイプ(配列)
日本, 東京都千代田区丸の内2丁目4−1: 35.68123509999999, 139.7639949: [sublocality_level_4, sublocality, political]

GASでない普通の環境ではGoogle Geocoding APIをHTTPで呼ぶところです。たとえば上のやり取りはブラウザでもできますし、アドレスバーをガチャガチャやるだけで好きな住所の経度緯度が取れます。

Google Drive上にファイルを保存する

  • フォルダを開くにはルートからイテレータをぐるぐるするか、ブラウザからIDを特定してDriveApp.getFolderById()する必要がある
  • 同じフォルダに同じ名前のファイルを複数作ることができる
  • ファイル作成時にMIMEタイプを指定する必要がある
  • ファイルの単純な上書きは動きが怪しかった

を踏まえて今はだいたい下記の感じです。削除したファイルはゴミ箱に行きます。

function createFile() {
  var fileName = "__test__.txt";
  var fileBody = "The quick brown fox jumped over the lazy dog.";
  var mimeType = "text/plain";
  var folderId = "root"; // root 以外の操作は、ブラウザでDriveを見る時にアドレスバーに載っているフォルダーIDが必要
  
  // フォルダオブジェクト取得
  var folder = DriveApp.getFolderById(folderId);
  
  // 既存の同名ファイルを削除
  var files = folder.getFilesByName(fileName);
  while (files.hasNext()) {
    var file = files.next();
    file.setTrashed(true);
    Utilities.sleep(5000);
  }
  
  // ファイル作成
  var file = folder.createFile(fileName, fileBody, mimeType);
}

Google Geocoding API利用メモ

APIのリクエスト回数

ジオコーディングは時間とリソースを消費するタスクです。可能な場合は、既知の住所をあらかじめ(ここで説明する Geocoding API や他のジオコーディング サービスを使用して)ジオコーディングし、独自に設計した一時キャッシュにその結果を保存するようにしてください。

(中略)

Google Geocoding API 使用時のクエリ制限として、1 日あたりの位置情報リクエストが 2,500 回に制限されています(Google Maps API for Business をご利用の場合は、1 日あたり 100,000 件までリクエストを実行できます)。

上記の通りです。Googleは同じ住所に対して都度ジオコーディングAPIを発行することを嫌っており、また1日のリクエスト上限は現在2500回です(時々思い出したように増えたり減ったりする)。あとこれは当たり前ですが秒間何十リクエストとか投げると一時的に蹴られます。

  • おこなった対策
    • 店舗マスタとジオコードキャッシュは別のシートで管理
    • ジオコーダのリクエスト間隔と起動間隔を開ける

マーカーがズレる

情報ソースに記載されている住所かGoogleの住所データベースが正確でない場合はどうしてもマーカーはズレます。

GoogleのDBに番地が登録されていなければ町のどこかまで、町が登録されていなければ市のどこかまで、市もなければ県のどこかまでズレます。

対策として、API応答のtypes[]に載ってくる区分の詳細さを示すsublocality_levelは1-5まであるので、これを精度レベルとしてマップに併記しています(部分マッチの効用などで全く違う住所にピンポイントでマッチした場合は効果がありません)。

  var accuracyTable = {
      "administrative_area_level_1": "-1(県単位)",
      "locality": "0(市区町村または郡単位)",
      "sublocality_level_1": "1(町単位)",
      "sublocality_level_2": "2(○○丁目単位)",
      "sublocality_level_3": "3(大まかな番地)",
      "sublocality_level_4": "4(細かな番地)",
      "sublocality_level_5": "5(ほぼ正確な住所)"
  }
  var styleTable = {
      "administrative_area_level_1": "#red",
      "locality": "#red",
      "sublocality_level_1": "#red",
      "sublocality_level_2": "#red",
      "sublocality_level_3": "#yellow",
      "sublocality_level_4": "#blue",
      "sublocality_level_5": "#blue"
  }

f:id:appalerm:20140923151448p:plain

マーカーを青から黄色にして精度がちょっと下がっていることを明示しています。

根本的には手動で住所やマーカー位置を直してやるなどの方法になるでしょう。

末尾に建物名が入っていると、妙な部分マッチが成立する

住所欄に建物や施設の名前が入っていることは特にゲーセンなどではしょっちゅうですが、建物名との部分マッチが成立して住所や番地の部分と関わりなく建物の場所が優先的に返ってくることがあります。

実例です。

"formatted_address" : "日本, 〒210-0007 神奈川県川崎市川崎区駅前本町 川崎モアーズ",

現在この2つは番地の違いを建物の名前で吸収し同じ結果を返します(川崎モアーズの正しい住所は後者)。思い通りの住所にマッチしていれば問題ありませんが、サイト作成時点では遠方の建物への誤ったマッチが無視できなかったのと番地が出ないのが気持ち悪いので対策しました。

premise: 名前のある場所を示します。通常は共通の名前を持つ建物や建物の集合体です。

建物や施設、ランドマークの類がマッチした場合、results[x].typesにはpolitical(行政区分)を含まずpremiseが格納されます。本サイトではtypesにpremiseを含むジオコーディング結果は無視することにしています。

現在は下記のようなアルゴリズムAPIリクエストをしています。

  • 末尾の文字をざくざく削りながら何度かリクエストして精度が下がったらbreak、最も精度の高い結果を採用する
    • 精度の求め方: results[x].address_componentsには番地、市区町村、都道府県、国の順で住所が分解されているので、配列の長さは相対的な精度と見なせる
    • typesにpremise(ランドマーク)が載っていたら結果を無視する

小字(こあざ)が入った住所で精度が下がる

小字(こあざ)とは、住所表記で言うA市B町C、町や丁目よりは下で番号よりは上の区分のあれです。

都市部では、かつて小字が存在していても[6]、区画整理事業や住居表示の導入によって小字は消滅していることが多い(大字は住居表示後の町名に引き継がれることが多い)。

Googleマップでは小字の存在を考慮しないことが多く、入力された住所に対して小字以降が無視された結果○○丁目程度の精度しか出ないことがあります。以下は千葉県のアーバンスクエア成東さんの住所をAPIに投げた結果です。Googleマップの検索窓でも似たような現象を見ることができます。

大字(おおあざ)にも似たような問題はありますが、こちらは大体Geocoding APIが表記の揺れとして吸収してくれます(たまに形態素解析に失敗して精度が一気に落ちることはあります)。

対策については、自力で住所をパースして小字以前と番号以降を分割するとかしてもまともな結果が出るよう自動化できるまで持って行くのは大量のデータを持って更新する羽目になり割に合いません。根本的には手動で住所やマーカー位置を直してやるなどの方法になるでしょう。

京都に弱い

京都市街などの碁盤目状の市街には「中京区寺町通四条上る」など特有の住所表記の法則があり、Google Geocoding APIはこれが単純にとても苦手です。

課題

  • Geocoderの精度
    • 他のAPIとの併用で緩和されるか解決する問題がある
  • ユーザー体験

まとめ

Googleは早くAndroid版MapsアプリのKMLリーダ機能復活させてください。早く。