住所+店舗名から緯度経度を取得する

ここ最近、店舗を地図にマッピングするアプリをいくつか作った。

千埼神割

千埼神割

  • Kensuke Hoshikawa
  • 旅行
  • 無料
apps.apple.com

apps.apple.com

このアプリを作った時、住所+店舗から緯度経度を収集した。ただの住所ではなく、個別の店舗の緯度経度となる。ややこしいのは住所の緯度経度と店舗の緯度経度は微妙にずれることである。

店舗の緯度経度を求めるには Google Map の Geocoding API でやるのが一番良さそうだが Google Map API は有料になる(この記事を書きながら再度調べたら無料枠があるっぽく、枠内で収まるならそれが良さそう)。

無料で住所から緯度経度を引くことを考えると、まず国土地理院APIが出てくる。しかし国土地理院APIだと店舗名に対応していない。ショッピングモールのように同じ住所にたくさんの店舗がある場合だと全て同じ緯度経度となってしまう。そこで Geocoding.jp という謎のAPIがあり、このAPIを利用すると Google Map のように店舗ごとに緯度経度を取得することができる。Geocoding.jp は裏側は Google / Yahoo となっているっぽい。ただ Geocoding.jp はほとんどの住所+店舗名から緯度経度をひけるのだが、一部取得できないことがある。

そこで、まず Geocoding.jp で緯度経度を取得し、取得できなかったら国土地理院から取得するようにした。

実装

TypeScript で実装した

Geocoding.jp から緯度経度を取得

  • Geocoding.jp は10秒間隔をあけてAPIを叩くこと
  • Geocoding.jp は緯度経度が0で返ってくることがあり、その場合はリトライで対応する
  • レスポンスが XML なので xml2js を使った
const xmlToJson = async (xml: string) => {
  const options = {
    trim: true,
    explicitArray: false,
  };

  return (await xml2js.parseStringPromise(xml.replace(/&/g, "&"), options))
    .result;
};

const getLatlngByGeocoding = async (
  address: string,
  name: string
): Promise<{ lat: number; lng: number }> => {
  await setTimeout(10000);
  const url = `https://www.geocoding.jp/api/?q=${address} ${name}`;
  const response = await fetch(url);
  const text = await response.text();
  const json = await xmlToJson(text);

  // 取得できなかったら国土地理院から取得
  if (
    json.coordinate === undefined ||
    json.coordinate.lat === undefined ||
    json.coordinate.lng === undefined
  ) {
    return getLatlngByGsi(address);
  } else {
    // 0 の時はリトライ
    if (json.coordinate.lat === "0" || json.coordinate.lng === "0") {
      return getLatlngByGeocoding(address, name);
    } else {
      return {
        lat: parseFloat(json.coordinate.lat),
        lng: parseFloat(json.coordinate.lng),
      };
    }
  }
};

国土地理院 API から緯度経度を取得

const getLatlngByGsi = async (
  address: string
): Promise<{ lat: number; lng: number }> => {
  const url = `https://msearch.gsi.go.jp/address-search/AddressSearch?q=${address}`;
  const response = await fetch(url);
  const text = await response.text();
  const coordinates = JSON.parse(text)[0].geometry.coordinates;
  return { lat: parseFloat(coordinates[1]), lng: coordinates[0] };
};

なお、緯度経度が重複した場合はアプリ上でピンが重なってしまうため、ちょっとだけ緯度経度をずらして重複がなくなるようにする必要がある。