🙆‍♀️

住所で検索可能な周辺情報 GraphQL/SPARQL API

2021/07/31に公開

Linked Open Addresses Japan という住所オープンデータ提供サイトで使用している周辺情報データを検索できる Web API を公開しました。

https://uedayou.net/loa/

実際には以下で使用しているものになります。

API には GraphQL と SPARQLエンドポイントの2種類があります。GraphQL は簡易、SPARQLエンドポイントはより詳細に検索ができます。用途によって使い分けてください。

特徴

一般的に、位置情報を持つ情報を検索する場合、緯度経度で範囲を絞り込んで検索することが一般的だと思います。今回公開する周辺情報 API は、住所オープンデータサイトで使用することも目的に作成しましたので、住所で検索できる必要がありました。そのため、全ての周辺情報について正規化された住所データを持たせています。
このAPIでは、緯度経度だけではなく住所での検索が可能であることが大きな特徴です。

周辺情報リスト

周辺情報はすべてオープンデータ由来のデータになりますので、比較的多目的に使いやすデータになっています。

種別 説明 データソース ライセンス
鉄道駅 日本の鉄道駅の位置と住所 鉄道駅LOD CC BY-SA
図書館 日本の図書館の位置と住所 図書館施設データポータル CC BY-SA
Wikidata Wikidata の格納された位置情報と住所を持つデータ Wikidata CC BY-SA
DBpedia 位置情報と住所を持つ WIkipediaの記事 DBpedia CC BY-SA

GraphQL

エンドポイント

https://uedayou.net/loa/search

クエリ

以下のクエリをPOSTしてください。

query PlaceList($query: String!, $type: String!) {
  searchPlaces(address: $query, type: $type) {
    label
    address
    url
    geohash
    type
  }
}

あわせて検索したい住所と検索タイプをvariablesオブジェクトに以下のように指定してください。
なお、GraphQLでの検索は住所の前方一致 のみ対応となります。例えば、東京都の有楽町の情報を得たい場合は、有楽町ではなく 東京都千代田区有楽町 と入力する必要があります。
部分一致検索や緯度経度などより詳細な検索をしたい場合は、後述のSPARQLエンドポイントを利用してください。

{
  query: "住所",
  type: "ALL", // "jrslod", "ldlib", "wikidata", "dbpedia"
}

type には以下が指定できます。

type 説明
ALL 全ての種類に対して検索
jrslod 鉄道駅
ldlib 図書館
wikidata Wikidata の場所データ
dbpedia Wikipedia の記事

例えば、GraphQLのクライアントソフトで検索すると、以下のように検索ができます。

SPARQL Endpoint

エンドポイント

https://uedayou.net/loa/nearby/sparql

エンドポイントを利用するには、GETで以下のパラメータを付加してください。

パラメータ 説明 備考
query URLエンコードしたSPARQLクエリ 必須
format json or xml 任意(デフォルト: json)

以下を利用すれば、WebページからSPARQLクエリを手軽に実行できます。

https://uedayou.net/ldapinavi/loaj-nearby

SPARQLクエリが良くわからない方は、クエリ例 から読んでもらえれば大丈夫です。
https://uedayou.net/ldapinavi/loaj-nearby に各クエリを入力して実行するだけで、GraphQL の検索結果とほぼ同じものが検索できます。

PREFIX

SPARQLクエリを使うときに以下の接頭辞を利用すると、クエリを短く書くことができます。

prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
prefix schema: <http://schema.org/>
prefix geo: <http://www.w3.org/2003/01/geo/wgs84_pos#>
prefix ic: <http://imi.go.jp/ns/core/rdf#>

Property

各種データのプロパティは以下のようになっています。

Property 値例
rdfs:label "国立国会図書館"@ja;
ic:住所 https://uedayou.net/loa/東京都千代田区永田町1丁目10
schema:address "東京都千代田区永田町1丁目10"
schema:geo http://geohash.org/xn76gzndg
geo:lat 35.92612981796265
geo:long 139.7973132133484

クエリ例

SPARQLでのクエリの事例をいくつか紹介します。全て、東京都千代田区有楽町周辺の情報を検索しています。

住所で検索

以下は、GraphQLでの検索と同等のものになります。
検索結果は、こちらから確認できます。

prefix ic: <http://imi.go.jp/ns/core/rdf#>
prefix schema: <http://schema.org/>
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
prefix geo: <http://www.w3.org/2003/01/geo/wgs84_pos#>

select distinct ?url ?label ?address ?loa ?geohash ?lat ?long where {
  ?url rdfs:label ?label;
     ic:住所 ?loa;
     schema:address ?address;
     schema:geo ?hashuri;
     geo:lat ?lat;
     geo:long ?long.
  bind(substr(str(?hashuri), 20) as ?geohash)
  filter( regex(?address, "東京都千代田区有楽町") )
} limit 1000

filter( regex(?address, "東京都千代田区有楽町") )

東京都千代田区有楽町を変更すれば任意の住所を検索できます。GraphQLとは違い、部分一致も可能です。例えば 有楽町 でも検索できます。

緯度経度の矩形で検索

検索結果は、こちらから確認できます。

prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
prefix schema: <http://schema.org/>
prefix geo: <http://www.w3.org/2003/01/geo/wgs84_pos#>
prefix ic: <http://imi.go.jp/ns/core/rdf#>

select distinct ?url ?label ?address ?loa ?geohash ?lat ?long where {
  ?url rdfs:label ?label;
     ic:住所 ?loa;
     schema:address ?address;
     schema:geo ?hashuri;
     geo:lat ?lat;
     geo:long ?long.
  bind(substr(str(?hashuri), 20) as ?geohash)
  filter(?lat > 35.664075 && ?lat < 35.678839 && ?long > 139.759628 && ?long < 139.772398)
} limit 1000

filter(?lat > 35.664075 && ?lat < 35.678839 && ?long > 139.759628 && ?long < 139.772398)

を変更すれば、任意の矩形で検索できます。

ジオハッシュで検索

検索結果は、こちらから確認できます。

prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
prefix schema: <http://schema.org/>
prefix geo: <http://www.w3.org/2003/01/geo/wgs84_pos#>
prefix ic: <http://imi.go.jp/ns/core/rdf#>

select distinct ?url ?label ?address ?loa ?geohash ?lat ?long where {
  ?url rdfs:label ?label;
     ic:住所 ?loa;
     schema:address ?address;
     schema:geo ?hashuri;
     geo:lat ?lat;
     geo:long ?long.
  bind(substr(str(?hashuri), 20) as ?geohash)
  filter( regex(str(?geohash), "^xn76uq") )
} limit 1000

プログラム上で検索

例えば、node.js でSPARQLエンドポイントを検索するには以下のようにします。

const axios = require('axios');

(async()=>{
  const endpoint = "https://uedayou.net/loa/nearby/sparql";
  const query = `
prefix ic: <http://imi.go.jp/ns/core/rdf#>
prefix schema: <http://schema.org/>
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
prefix geo: <http://www.w3.org/2003/01/geo/wgs84_pos#>

select distinct ?url ?label ?address ?loa ?geohash ?lat ?long where {
  ?url rdfs:label ?label;
     ic:住所 ?loa;
     schema:address ?address;
     schema:geo ?hashuri;
     geo:lat ?lat;
     geo:long ?long.
  bind(substr(str(?hashuri), 20) as ?geohash)
  filter( regex(?address, "東京都千代田区有楽町") )
} limit 1000
`;
  try {
    const res = await axios(`${endpoint}?query=${encodeURIComponent(query)}`);
    console.log(JSON.stringify(res.data, null, 2));
  } catch (e) {
    console.error(e);
  }
})();

サンプルプログラム

検索結果を地図とリストに表示できるサンプルプログラムをVue.jsVuetifyで作成しました。GraphQL、SPARQLエンドポイントともに以下のように表示されます。

コードは以下のリポジトリでも公開しています。

https://github.com/uedayou/loa-nearby-api-sample-app

この API を使う際の参考になればと思います。

GraphQL の例

https://uedayou.github.io/loa-nearby-api-sample-app/src/graphqll/index.html

<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
  <title>周辺情報リスト</title>
  <link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
  <link href="https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" rel="stylesheet">
  <link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">
  <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
</head>
<body>
  <div id="app">
    <v-app>
      <v-main>
        <v-container class="pt-10 pb-5">
          <h1 class="text-center py-5">{{ query }} の周辺情報</h1>
          <v-divider></v-divider>
          <l-map style="height: 350px;" :bounds="bounds">
            <l-tile-layer :url="url"></l-tile-layer>
            <l-marker v-for="place in places" :lat-lng="{lat: place.coord.latitude, lon: place.coord.longitude}">
            <l-popup>
              <a :href="place.url" target="_blank" rel="noopener noreferrer">{{ place.label }}</a>
            </l-popup>
            </l-marker>
          </l-map>
          <v-simple-table>
            <template v-slot:default>
              <thead>
                <tr class="text-center">
                  <th>Label</th>
                  <th>Address</th>
                  <th>Geohash</th>
                  <th>Type</th>
                </tr>
              </thead>
              <tbody>
                <tr v-for="place in places" :key="place.label">
                  <td>
                    <a :href="place.url" target="_blank" rel="noopener noreferrer">{{ place.label }}</a>
                  </td>
                  <td>
                    <a :href="`http://uedayou.net/loa/${place.address}`" target="_blank" rel="noopener noreferrer">{{ place.address }}</a>
                  </td>
                  <td>
                    <a :href="`http://geohash.org/${place.geohash}`" target="_blank" rel="noopener noreferrer">{{ place.geohash }}</a>
                    <p class="caption">{{ place.coord.latitude.toFixed(8) }},{{ place.coord.longitude.toFixed(8) }}</p>
                  </td>
                  <td class="text-center">
                    {{ place.type }}
                  </td>
                </tr>
              </tbody>
            </template>
          </v-simple-table>
        </v-container>
      </v-main>
    </v-app>
    <v-overlay :value="isLoading" style="z-index:999">
      <v-progress-circular indeterminate size="64"></v-progress-circular>
    </v-overlay>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
  <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
  <script src="https://unpkg.com/vue2-leaflet"></script>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/@alexpavlov/geohash-js"></script>
  <script>
    Vue.component('l-map', window.Vue2Leaflet.LMap);
    Vue.component('l-tile-layer', window.Vue2Leaflet.LTileLayer);
    Vue.component('l-marker', window.Vue2Leaflet.LMarker);
    Vue.component('l-popup', window.Vue2Leaflet.LPopup);

    var API_URL = "https://uedayou.net/loa/search";
    var QUERY = "東京都千代田区有楽町";
    var TYPE = "ALL"; // "jrslod", "ldlib", "wikidata", "dbpedia"
    new Vue({
      el: '#app',
      vuetify: new Vuetify(),
      data() {
        return {
          places: [],
          query: QUERY,
          type: TYPE,
          isLoading: false,
          url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
          bounds: null,
        }
      },
      mounted() {
        var self = this;
        self.isLoading = true;
        axios.post(
          API_URL, {
            query: 
              `query PlaceList($query: String!, $type: String!) {
                searchPlaces(address: $query, type: $type) {
                  label
                  address
                  url
                  geohash
                  type
                }
              }`,
            variables: {
              query: self.query,
              type: self.type,
            }
        })
        .then(function(response) {
          self.places = response.data.data.searchPlaces.map(function(place) {
            place.coord = geohash.decode(place.geohash);
            self.setBounds(place.coord.latitude, place.coord.longitude);
            return place;
          });
        }).catch(function(error) {
          console.log(error);
        }).finally(function() {
          self.isLoading = false;
        });
      },
      methods: {
        setBounds: function(lat, long) {
          if (!this.bounds) this.bounds = [[lat, long],[lat, long]];
          if (this.bounds[1][0] > lat) this.bounds[1][0] = lat;
          if (this.bounds[0][0] < lat) this.bounds[0][0] = lat;
          if (this.bounds[1][1] > long) this.bounds[1][1] = long;
          if (this.bounds[0][1] < long) this.bounds[0][1] = long;
        }
      }
    })
  </script>
</body>
</html>

SPARQL エンドポイントの例

https://uedayou.github.io/loa-nearby-api-sample-app/src/sparql/index.html

<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
  <title>周辺情報リスト</title>
  <link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
  <link href="https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" rel="stylesheet">
  <link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">
  <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
</head>
<body>
  <div id="app">
    <v-app>
      <v-main>
        <v-container class="pt-10 pb-5">
          <h1 class="text-center py-5">周辺情報</h1>
          <v-divider></v-divider>
          <l-map style="height: 350px;" :bounds="bounds">
            <l-tile-layer :url="url"></l-tile-layer>
            <l-marker v-for="place in places" :lat-lng="{lat: place.coord.latitude, lon: place.coord.longitude}">
            <l-popup>
              <a :href="place.url.value" target="_blank" rel="noopener noreferrer">{{ place.label.value }}</a>
            </l-popup>
            </l-marker>
          </l-map>
          <v-simple-table>
            <template v-slot:default>
              <thead>
                <tr class="text-center">
                  <th>Label</th>
                  <th>Address</th>
                  <th>Geohash</th>
                </tr>
              </thead>
              <tbody>
                <tr v-for="place in places" :key="place.label.value">
                  <td>
                    <a :href="place.url.value" target="_blank" rel="noopener noreferrer">{{ place.label.value }}</a>
                  </td>
                  <td>
                    <a :href="`http://uedayou.net/loa/${place.address.value}`" target="_blank" rel="noopener noreferrer">{{ place.address.value }}</a>
                  </td>
                  <td>
                    <a :href="`http://geohash.org/${place.geohash.value}`" target="_blank" rel="noopener noreferrer">{{ place.geohash.value }}</a>
                    <p class="caption">{{ place.coord.latitude.toFixed(8) }},{{ place.coord.longitude.toFixed(8) }}</p>
                  </td>
                </tr>
              </tbody>
            </template>
          </v-simple-table>
        </v-container>
      </v-main>
    </v-app>
    <v-overlay :value="isLoading" style="z-index:999">
      <v-progress-circular indeterminate size="64"></v-progress-circular>
    </v-overlay>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
  <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
  <script src="https://unpkg.com/vue2-leaflet"></script>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/@alexpavlov/geohash-js"></script>
  <script>
    var API_URL = "https://uedayou.net/loa/nearby/sparql";
    var QUERY = `
prefix ic: <http://imi.go.jp/ns/core/rdf#>
prefix schema: <http://schema.org/>
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
prefix geo: <http://www.w3.org/2003/01/geo/wgs84_pos#>

select distinct ?url ?label ?address ?loa ?geohash ?lat ?long where {
?url rdfs:label ?label;
     ic:住所 ?loa;
     schema:address ?address;
     schema:geo ?hashuri;
     geo:lat ?lat;
     geo:long ?long.
  bind(substr(str(?hashuri), 20) as ?geohash)
  filter( regex(?address, "東京都千代田区有楽町") )
} limit 1000
`;

    Vue.component('l-map', window.Vue2Leaflet.LMap);
    Vue.component('l-tile-layer', window.Vue2Leaflet.LTileLayer);
    Vue.component('l-marker', window.Vue2Leaflet.LMarker);
    Vue.component('l-popup', window.Vue2Leaflet.LPopup);
    new Vue({
      el: '#app',
      vuetify: new Vuetify(),
      data() {
        return {
          places: [],
          query: QUERY,
          isLoading: false,
          url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
          bounds: null,
        }
      },
      mounted() {
        var self = this;
        self.isLoading = true;
        axios.get(
          API_URL+"?query="+encodeURIComponent(QUERY)
        ).then(function(response) {
          self.places = response.data.results.bindings.map(function(place) {
            place.coord = geohash.decode(place.geohash.value);
            self.setBounds(place.coord.latitude, place.coord.longitude);
            return place;
          });
        }).catch(function(error) {
          console.log(error);
        }).finally(function() {
          self.isLoading = false;
        });
      },
      methods: {
        setBounds: function(lat, long) {
          if (!this.bounds) this.bounds = [[lat, long],[lat, long]];
          if (this.bounds[1][0] > lat) this.bounds[1][0] = lat;
          if (this.bounds[0][0] < lat) this.bounds[0][0] = lat;
          if (this.bounds[1][1] > long) this.bounds[1][1] = long;
          if (this.bounds[0][1] < long) this.bounds[0][1] = long;
        }
      }
    })
  </script>
</body>
</html>

Discussion