住所で検索可能な周辺情報 GraphQL/SPARQL API
Linked Open Addresses Japan という住所オープンデータ提供サイトで使用している周辺情報データを検索できる Web API を公開しました。
実際には以下で使用しているものになります。
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クエリを手軽に実行できます。
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.jsとVuetifyで作成しました。GraphQL、SPARQLエンドポイントともに以下のように表示されます。
コードは以下のリポジトリでも公開しています。
この 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