住所を緯度経度に変換、緯度経度を住所に変換する
住所から緯度経度に変換する作業のことをジオ・コーディング(geocoding)といい、その逆の作業(緯度経度から住所に変換)をリバース・ジオコーディング(reverse geocoding)といいます。ここでは、Pythonを使ってジオ・コーディングとリバース・ジオコーディングをする方法を説明します。
OpenStreetMapとNominatim
以下では、OpenStreetMap(OSM)のデータを利用して、ジオ・コーディングとリバース・ジオコーディングをします。OpenStreetMapは、完全に無料の地図検索、地理情報検索のサービスです。Google Mapsなど、企業が運営するサービスでもジオ・コーディングとリバース・ジオコーディングは可能ですが、準備や設定にひと手間、ふた手間かかるので、ここではOpenStreetMapを使います。
より具体的には、Nominatimというサービス(無料)を使います。Nominatimは、OpenStreetMapの地図・地理情報を使ってジオ・コーディングとリバース・ジオコーディングをする検索エンジンです。
Nominatimは無料ですが、1秒に1度までしか検索できません。ラズパイを使ったホビー・プロジェクトでこの制限が大問題なることはまずありません。
GeoPyの準備
PythonのプログラムからNominatimを使うには、GeoPyというモジュールを使うとよいと思います。モジュールというのは、いくつかの機能をひとまとめにした集合体のことです。GeoPyは、Nominatimやその他の検索エンジンを使ってジオ・コーディングとリバース・ジオコーディングをする機能をひとまとめにしたモジュールです。
GeoPyをラズパイにインストール前に、ラズパイOSを最新の状態にアップデートしてください。Terminalで以下のコマンドを実行します。
sudo apt update -y
続いて、以下のコマンドを実行:
sudo apt full-upgrade -y
これで、ラズパイOSが最新の状態にアップデートされました。
GeoPyは、以下のコマンドでインストールします。
sudo pip3 install geopy
住所から緯度経度へ変換(ジオ・コーディング)
GeoPyをインストールし終わったら、geocoding.pyをラズパイ上のThonnyへコピーして保存してください。
GeoPyを使ってNominatimを使うには、以下の行をプログラムの先頭部分に書きます。詳細は避けますが、GeoPyを使ってNominatimを使うための「おまじない」だと思っておいてください。
from geopy.geocoders import Nominatim
そして3行目
appName = "panda-admirer"
の”panda-admirer”の部分を、別のものに変えてください。自分のニックネーム、自分のアプリの名前、自分のラズパイの名前など、何でも構いません。”123”や”abc”などでも構いませんが、他の人も同じものを使っているかもしれないので、もう少しspecificな(自分固有の)ネーミングがいいと思います。なお、左はしと右はしのダブルクオーテーション(”)は消さずに残してください。
この情報を使い、以下のようにしてNominatimを使う準備をします。
geolocator = Nominatim(user_agent=appName)
プログラムを実行してみてください。Thonnyの「Shell」の部分に、ズラズラっと文字が表示されるはずです。それが、Nominatimで検索(ジオ・コーディング)した結果です。
このプログラムでは、3種類の方法で検索(ジオ・コーディング)をしています。
- キーワードを使った検索
- 住所を使った検索
- 詳細検索
まずは、有名な場所や建物の名前をキーワードとして使う検索(ジオ・コーディング)を説明します。キーワードとしては、駅名、学校名、レストラン名、ホテル名など、さまざまなものが使えます。geocoding.pyでは、「Fenway Park」を検索キーワードにして、以下のようにジオ・コーディングしています。
searchKeywords = "Fenway Park"
location = geolocator.geocode(query=searchKeywords,
addressdetails=True)
print(location.address)
locationDataset = location.raw
print("Fenway's latitude and longitude: " +
locationDataset["lat"] + ", " +
locationDataset["lon"])
このように、ジオ・コーディングする時には、geocode()という関数(function)を使います。「query=・・・」として検索キーワードをgeocode()に渡します。「・・・」の部分が検索キーワードです。検索キーワードを使ったジオ・コーディングでは、geocode()に「addressdetails=True」というのも渡すのがおすすめです。
geocode()は、ジオ・コーディングした結果を返します。上の例では、それがlocationという変数に格納され、location.addressが(Fenwayの)住所になります。ThonnyのShellに以下の住所が表示されるはずです。
Fenway Park, 4, Jersey Street, Audubon Square, Boston, Suffolk County, Massachusetts, 02115, United States
住所だけでなく、すべての結果をみるには、location.rawを使います。これはディクショナリーになっていて、上の例ではlocationDatasetに格納されます。緯度を得るにはlocationDataset[“lat”]、経度を得るにはlocationDataset[“lon”]とします。緯度、経度とも、十進法(小数)表記になっています。ThonnyのShellに以下のように表示されるはずです。
Fenway's latitude and longitude: 42.3464621, -71.0971002033302
ディクショナリーの使い方については、「リストとディクショナリー(辞書)」を参照してください。十進法(小数)表記の緯度、軽度についてはここを参照のこと。
キーワード検索以外に、住所が分かっている場合にはそれを使った検索も可能です。以下のようにして、住所を文字列として表記し、それをgeocode()に渡します。
fenwayAddress = "4 Jersey Street, Boston, MA 02215"
location = geolocator.geocode(query=fenwayAddress)
住所を文字列として表記するのではなく、もっと正確に(明示的に)表記することもできます。住所の情報のうち、どこの部分が何を表しているかはっきりさせたい場合に使います。例えばFenwayの住所のうち、「4 Jersey St」がストリートアドレス、「Boston」が都市名、という風に明記できます。以下のように、住所をディクショナリーとして表現し(文字列ではなく)、そのディクショナリーをgeocode()に渡します。
structuredQuery = { "street" : "4 Jersey St",
"city" : "Boston",
"state" : "MA",
"postalcode" : "02215",
"country" : "United States" }
location = geolocator.geocode(query=structuredQuery)
緯度経度から住所へ変換(リバース・ジオコーディング)
次に、十進法(小数)表記の緯度、軽度から住所へ変換するリバース・ジオコーディングのやり方を説明します。reverse-geocoding.pyをラズパイ上のThonnyへコピーして保存してください。最初の4行については前のセクションで説明したので、ここでは説明を省略します。
リバース・ジオコーディングをするには、緯度と経度が事前に分かっているのが前提です。ここでは、次のように緯度42.34671602665354、経度-71.09718561498757を使います。これは、フェンウェイ・パークの緯度、経度です。
fenwayLatitude = 42.34671602665354
fenwayLongitude = -71.09718561498757
ちなみに、Google Mapsの地図上で右クリックをすると緯度、経度が表示され、それをクリックすると、緯度、経度をコピーできます(以下写真参照)。
リバース・ジオコーディングをするには、reverse()という関数(function)を使います。以下のように、緯度と経度をペアにしてreverse()に渡します。
location = geolocator.reverse( query=(fenwayLatitude, fenwayLongitude) )
print(location.address)
reverse()は、リバース・ジオコーディングした結果を返します。上の例では、それがlocationという変数に格納され、location.addressが(Fenwayの)住所になります。ThonnyのShellに以下の住所が表示されるはずです。
Fenway Park, 4, Jersey Street, Audubon Square, Boston, Suffolk County, Massachusetts, 02115, United States
住所をひとつの文字列として得るのではなく、住所の情報のうち一部だけ切り出すこともできます。例えば、郵便番号だけ知りたいとか、州の名前と都市名だけ知りたいというような場合です。リバース・ジオコーディングした結果すべては、location.rawとすると見ることができます。これはディクショナリーになっていて、下の例ではlocationDatasetに格納されます。これを使ってlocationDataset[“address”]とすると、住所情報がディクショナリーとして得られ、以下の例ではaddressに格納されます。
locationDataset = location.raw
address = locationDataset["address"]
if "city" in address:
print(address["city"])
if "town" in address:
print(address["town"])
print(address["county"])
print(address["state"])
print(address["postcode"])
print(address["country"])
print(locationDataset)
住所情報から都市名を得るにはaddress[“city”]、タウン名を得るにはaddress[“town”]、カウンティ名を得るにはaddress[“county”]、州名を得るにはaddress[“state”]、郵便番号を得るにはaddress[“postcode”]、国名を得るにはaddress[“country”]とします。
住所の中に都市名が含まれている場合、それはaddress[“city”]として得ることができます。例えば、ボストンは都市(city)なので(タウンではない)、ボストン市内の住所には「Boston」という都市名が含まれています。その都市名を得るには、address[“city”]とします。一方、ボストン市内の住所にはタウン名は含まれていません。したがって、address[“town”]というふうに、その住所からタウン名を得ようとするとエラーが起きます。
ある住所情報の中に都市名とタウン名のどちらが含まれているのか、分からない場合があります(多くの場合はそうだと思います)。そのときは以下のようにして、住所の中に都市名が含まれていればaddress[“city”]として都市名を得て、タウン名が含まれていればaddress[“town”]としてタウン名を得ます。
if "city" in address:
print(address["city"])
if "town" in address:
print(address["town"])
reverse-geocoding.pyでは、フェンウェイ・パークの緯度、経度をリバース・ジオコーディングした後、日本語学校のアーリントン事務所の緯度、経度をリバース・ジオコーディングしています。ほとんど同じプログラムの繰り返しなので、説明は省略します。以下2点だけ注目して欲しいことを書いておきます。
ひとつは、リバース・ジオコーディングの精度について。reverse-geocoding.pyを実行すると、住所が
800, Massachusetts Avenue, Arlington, MA
となるのですが、日本語学校の事務所の本当の住所は
792 Massachusetts Ave, Arlington, MA 02476
です。このように、リバース・ジオコーディング(のアルゴリズム)には常に多少の誤差が含まれます。ストリート・アドレス(番地)まで正確にリバース・ジオコーディングできる場合と、できない場合があると理解しておいてください(上記のフェンウェイ・パークの住所は番地まで正確でした)。
もうひとつは、アーリントンがタウンだということ(cityではない)。アーリントン内の住所には「Arlington」というタウン名が含まれています。そのタウン名を得るには、address[“town”]とします。一方、アーリントン内の住所には都市名は含まれていません。したがって、address[“city”]というふうに、その住所から都市名を得ようとするとエラーが起きます。