国土地理院の標高APIの使い方:「-----」の返り方、hsrcの精度、大量の地点は標高タイルで
要点
- URLは1本、キーは不要。
https://cyberjapandata2.gsi.go.jp/general/dem/scripts/getelevation.php?lon=経度&lat=緯度&outtype=JSONをGETすると、elevationとhsrcが返ります。 - 失敗もHTTP 200。 海上・国外・緯度と経度の取り違え・
lonの省略は、どれも文字列の"-----"でした。成功時のelevationは数値です。 - hsrc を値と一緒に残す。 1m(レーザ)から10mまで5種類あり、桁数も違います。同じ断面の中でも地点ごとに変わりました。
- 大量の地点は標高タイル。 PNGの画素から標高を計算すると、7地点すべてでAPIと同じ値になり、51地点の断面はタイル12回の取得で済みました。
国土地理院の標高APIは、緯度経度を渡すと、その地点の標高と、もとになったデータの種類(hsrc)を返します。 取れないときもHTTP 200で、elevation が数値ではなく "-----" という文字列になります。ステータスコードだけを見て成功と判断すると、この文字列を標高として扱ってしまいます。
この記事は、2026年10月20日にNode.js(依存なし)とcurlから、標高APIを14回、標高タイルを30回、逆ジオコーダーを3回呼び出した結果です。住所から緯度経度を求める側は国土地理院の住所検索APIと逆ジオコーダーの使い方にまとめています。
リクエストと応答の形
説明ページのパラメータは4つです。lon(経度)と lat(緯度)は十進の度で必須、outtype=JSON か callback(JSONPの関数名)のどちらか一方を付けます。東京駅の丸の内駅前広場付近で呼び出しました。
curl -s "https://cyberjapandata2.gsi.go.jp/general/dem/scripts/getelevation.php?lon=139.767125&lat=35.681236&outtype=JSON"{"elevation":3.6,"hsrc":"1m(レーザ)"}hsrc は日本語が \uXXXX でエスケープされていて、JSON.parse すると 1m(レーザ) になります。括弧は全角です。応答は application/json; charset=utf-8 でした。
説明ページと実際の挙動で違った点が2つあります。
outtypeを省いてもJSONが返った。callbackもouttypeも付けないリクエストで、同じJSONがapplication/jsonで返りました。説明どおりouttype=JSONを付けておくのが無難です。- ブラウザからも呼べる。
Originヘッダーを付けて送るとAccess-Control-Allow-Origin: *が返りました。JSONPを使わなくても、別のオリジンのfetchで読めます。
callback=myfunc を付けると、application/javascript で myfunc({"elevation":3.6,...}) が返りました。
8地点と失敗の返り方
| 地点 | 緯度, 経度 | elevation | hsrc | 応答時間 |
|---|---|---|---|---|
| 国土地理院(説明ページの使用例の座標) | 36.103543, 140.08531 | 25.2 | 5m(レーザ) | 247ms |
| 富士山 剣ヶ峰付近 | 35.360625, 138.727363 | 3774.8 | 1m(レーザ) | 48ms |
| 東京駅 丸の内駅前広場付近 | 35.681236, 139.767125 | 3.6 | 1m(レーザ) | 48ms |
| 江戸川区 船堀二丁目 | 35.6838, 139.8573 | -0.8 | 1m(レーザ) | 68ms |
| 琵琶湖の湖上 | 35.25, 136.08 | 85 | 10m | 184ms |
| 旭岳付近 | 43.6634, 142.8544 | 2286.9 | 5m(レーザ) | 132ms |
| 海上 | 33.0, 137.0 | "-----" | "-----" | 221ms |
| ソウル市庁付近 | 37.5665, 126.978 | "-----" | "-----" | 194ms |
江戸川区の地点名は逆ジオコーダーで確かめました(muniCd 13123、lv01Nm 船堀二丁目)。応答時間は1回ずつの計測で、2回目以降は中継サーバーのキャッシュに当たった可能性があります。
入力の誤りも、同じ "-----" で返りました。
緯度と経度を逆に渡す(lat=139.767125&lon=35.681236) -> {"elevation":"-----","hsrc":"-----"} HTTP 200lon を省く(lat=35.681236 のみ) -> {"elevation":"-----","hsrc":"-----"} HTTP 200読み取れることは3つです。
- 型で判定する。 成功時の
elevationは数値、失敗時は文字列です。Number("-----")はNaNなので、そのまま計算に入れると後で気づきにくい壊れ方をします。 - 入力の誤りと「データがない」が区別できない。 海上も、緯度経度の取り違えも、同じ応答です。呼ぶ前に、緯度経度が日本の範囲に入っているかを自分で確かめます。
- 負の値は正常な値。 江戸川区の地点は -0.8 でした。0未満を異常値として捨てると、海抜ゼロメートル地帯の値を失います。
hsrc の種類と桁数
説明ページは、hsrc を精度の高い順に次の5つとしています。その地点で最も精度の高いデータの値を返すとあります。
| hsrc | 実測で返った例 | 返った桁 |
|---|---|---|
| 1m(レーザ) | 富士山 3774.8、東京駅 3.6、江戸川区 -0.8 | 0.1m |
| 5m(レーザ) | 国土地理院 25.2、旭岳 2286.9 | 0.1m |
| 5m(写真測量) | 今回の8地点にはなかった | — |
| 5m(写真測量5C) | 今回の8地点にはなかった | — |
| 10m | 琵琶湖の湖上 85 | 1m |
説明ページの表には、1m(レーザ)は0.1mの位まで、10mは1mの位までとあります。5m(レーザ)は実測で0.1mの位まででした。標高を比べるときは、hsrc が違う値どうしの差が、データの違いによるものかもしれない点に注意します。
標高タイルから同じ値を計算する
標高APIを1点ずつ呼ぶ代わりに、地理院タイルの標高タイルを取得して、画素から標高を計算する方法があります。標高タイルの詳細仕様とタイル一覧で確かめた要点です。
- PNGは1タイル256×256画素。 画素のRGBから
x = 2^16 × R + 2^8 × G + Bを計算し、xが2^23未満ならx × 0.01m、2^23より大きければ(x − 2^24) × 0.01m です。(128, 0, 0)(x = 2^23)は無効値です。 - データごとにURLとズームレベルが違う。 DEM1A は
dem1a_png(1〜17)、DEM5A・5B・5C はdem5a_pngなどの3つ(1〜15)、DEM10B はdem_png(1〜14)です。 - テキスト形式は更新停止。 タイル一覧に「テキスト形式の標高タイルは令和6年10月より更新を停止」とあるため、PNG形式を使います。
取得したPNGは8bitのRGB(カラータイプ2)で、東京駅付近のDEM1Aのタイルは14,434バイトでした。Node.jsの組み込みの zlib だけで読めます。
// dem-tile.mjs — 国土地理院の標高タイル(PNG)から1点の標高を読む。依存なし(Node.js 18 以降)import { inflateSync } from 'node:zlib';
const UA = 'dem-tile-example/0.1 (+https://codeagent.jp/)';// 精度の高い順。標高APIのhsrcの優先順に合わせるexport const SOURCES = [ { id: 'dem1a_png', z: 17, hsrc: '1m(レーザ)' }, { id: 'dem5a_png', z: 15, hsrc: '5m(レーザ)' }, { id: 'dem5b_png', z: 15, hsrc: '5m(写真測量)' }, { id: 'dem5c_png', z: 15, hsrc: '5m(写真測量5C)' }, { id: 'dem_png', z: 14, hsrc: '10m' },];
// 緯度経度 → タイル番号と、タイル内のピクセル位置(Webメルカトル)export function locate(lat, lon, z) { const n = 2 ** z; const rad = (lat * Math.PI) / 180; const fx = ((lon + 180) / 360) * n; const fy = ((1 - Math.log(Math.tan(rad) + 1 / Math.cos(rad)) / Math.PI) / 2) * n; const x = Math.floor(fx), y = Math.floor(fy); return { x, y, px: Math.floor((fx - x) * 256), py: Math.floor((fy - y) * 256) };}
// 8bit RGB / RGBA のPNGだけを扱う最小のデコーダーexport function decodePng(buf) { if (buf.readUInt32BE(0) !== 0x89504e47) throw new Error('PNGではない'); let pos = 8, width, height, colorType, idat = []; while (pos < buf.length) { const len = buf.readUInt32BE(pos); const type = buf.toString('ascii', pos + 4, pos + 8); const data = buf.subarray(pos + 8, pos + 8 + len); if (type === 'IHDR') { width = data.readUInt32BE(0); height = data.readUInt32BE(4); const depth = data[8]; colorType = data[9]; if (depth !== 8 || ![2, 6].includes(colorType) || data[12] !== 0) throw new Error(`未対応のPNG: depth=${depth} color=${colorType} interlace=${data[12]}`); } else if (type === 'IDAT') idat.push(data); else if (type === 'IEND') break; pos += 12 + len; } const bpp = colorType === 6 ? 4 : 3; const raw = inflateSync(Buffer.concat(idat)); const stride = width * bpp; const out = Buffer.alloc(stride * height); for (let y = 0; y < height; y++) { const filter = raw[y * (stride + 1)]; const line = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1)); for (let i = 0; i < stride; i++) { const a = i >= bpp ? out[y * stride + i - bpp] : 0; // 左 const b = y > 0 ? out[(y - 1) * stride + i] : 0; // 上 const c = y > 0 && i >= bpp ? out[(y - 1) * stride + i - bpp] : 0; // 左上 let v = line[i]; if (filter === 1) v += a; else if (filter === 2) v += b; else if (filter === 3) v += (a + b) >> 1; else if (filter === 4) { const p = a + b - c, pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c); v += pa <= pb && pa <= pc ? a : pb <= pc ? b : c; } out[y * stride + i] = v & 0xff; } } return { width, height, bpp, pixels: out };}
// 標高タイルの詳細仕様の式: x = 2^16R + 2^8G + B、x = 2^23 は無効値、x > 2^23 は負の値export function rgbToElevation(r, g, b) { const x = r * 65536 + g * 256 + b; if (x === 8388608) return null; return Math.round((x < 8388608 ? x : x - 16777216)) / 100;}
const cache = new Map();async function fetchTile(id, z, x, y) { const url = `https://cyberjapandata.gsi.go.jp/xyz/${id}/${z}/${x}/${y}.png`; if (cache.has(url)) return cache.get(url); const res = await fetch(url, { headers: { 'User-Agent': UA } }); const result = res.ok ? { url, status: res.status, png: decodePng(Buffer.from(await res.arrayBuffer())) } : { url, status: res.status, png: null }; cache.set(url, result); return result;}
// 精度の高いタイルから順に見て、最初に値があったものを返すexport async function elevationFromTiles(lat, lon, { onFetch } = {}) { const tried = []; for (const s of SOURCES) { const { x, y, px, py } = locate(lat, lon, s.z); const t = await fetchTile(s.id, s.z, x, y); onFetch?.(t); if (!t.png) { tried.push({ source: s.id, status: t.status }); continue; } const i = (py * t.png.width + px) * t.png.bpp; const h = rgbToElevation(t.png.pixels[i], t.png.pixels[i + 1], t.png.pixels[i + 2]); tried.push({ source: s.id, status: t.status, value: h }); if (h !== null) return { elevation: h, hsrc: s.hsrc, tile: t.url, pixel: [px, py], tried }; } return { elevation: null, hsrc: null, tried };}データがない範囲のタイルは、HTTP 404 が返りました。そのときは次に精度の低いデータへ進みます。タイルはあっても画素が無効値の場合も同じです。
7地点でAPIと比べる
前の表の陸地6地点と海上1地点で、タイルから計算した値とAPIの値を並べました。
| 地点 | タイルの値 | 使ったデータ | APIの値 |
|---|---|---|---|
| 国土地理院 | 25.24 | dem5a_png(dem1a_png は404) | 25.2 |
| 富士山 剣ヶ峰付近 | 3774.84 | dem1a_png | 3774.8 |
| 東京駅 丸の内駅前広場付近 | 3.60 | dem1a_png | 3.6 |
| 江戸川区 船堀二丁目 | -0.79 | dem1a_png | -0.8 |
| 琵琶湖の湖上 | 84.60 | dem_png(1m・5mの4つは404) | 85 |
| 旭岳付近 | 2286.89 | dem5a_png(dem1a_png は404) | 2286.9 |
| 海上 | なし | 5つとも404 | "-----" |
タイルの値を hsrc の桁で丸めると、7地点すべてでAPIの値と一致しました。 使ったデータの種類も hsrc と同じです。標高APIが内部でどう計算しているかを書いた資料は見つけていないので、「この7地点では同じ値になった」という結果として読んでください。この比較で、タイルへのリクエストは17回(200が6回、404が11回)でした。
51地点の断面:APIなら51回、タイルなら12回
東京駅の丸の内駅前広場付近(35.681236, 139.767125)から、西へ約1.1kmの地点(35.6800, 139.7545。逆ジオコーダーの町字名は「千代田」)までを51地点に分けて、タイルから標高を計算しました。
同じ処理を標高APIで行うと51回の呼び出しになります。タイルは1枚で256×256画素を持つので、近い地点が並ぶ断面や面の計算では、取得回数が地点数よりずっと少なくなります。
もう一つ分かったのは、1本の断面の中でも hsrc が変わることです。51地点のうち6地点は、DEM1AとDEM5Aのタイルの該当画素が無効値で、10mのデータの値になりました。終点は標高APIでも確かめ、{"elevation":8,"hsrc":"10m"}(タイルの値7.77を1m単位にした値)が返りました。断面図を描くときは、地点ごとに hsrc を残し、データの種類が変わる区間を区別して表示します。
AIエージェント・MCPツールにするときの設計
- 呼ぶ前に入力を確かめる。 緯度と経度の取り違えは
"-----"になり、エラーとしては返りません。日本の範囲を大まかに外れる値や、緯度に100を超える値が入っていたら、APIを呼ばずに止めます。 - 失敗を
nullと理由に変換する。elevationが数値でなければ{ elevation: null, reason: "no_data" }のように返し、0や前の地点の値で埋めません。 hsrcと取得元を返す。 値と一緒にhsrc、取得元URL、取得日時を返します。出典の型はMCPの出典情報を欠落させないoutputSchema設計に合わせられます。- 多数の地点はタイルにし、間隔はサーバー側で決める。 1点ずつAPIを呼ぶのは少数の問い合わせに限ります。上限や待機をモデルに任せない理由は公共データAPIをMCP化する設計パターンに書きました。
- 標高を安全の判断に使わせない。 返る値は数値標高モデルの値で、建物の上の高さや浸水の危険度ではありません。避難や浸水の判断が必要な質問には、自治体のハザードマップなどの公式の情報を確認するよう返します。
- 標高の回答は get_elevation の結果だけを根拠にし、hsrc と出典を併記する- elevation が null の地点は「データなし」と書き、近くの地点の値で補わない- 緯度と経度の取り違えが疑われる入力(緯度が90を超えるなど)は、呼び出さずに確認を求める- 浸水や避難の可否を標高だけで判断しない- 出典は「地理院タイル(標高タイル)を加工して作成」または国土地理院の標高APIのURLで残す住所から標高を引きたいときは、住所検索APIで緯度経度を求めてから渡します。住所検索の座標は代表点の場合があるため、どの粒度の座標の標高かも結果に残してください。
まとめ
- 標高APIは
getelevation.php?lon=&lat=&outtype=JSON。キー不要。成功は数値、失敗は文字列の"-----"で、どちらもHTTP 200 - 海上・国外・緯度と経度の取り違え・
lonの省略は、すべて同じ"-----"。呼ぶ前に入力を確かめる hsrcは1m(レーザ)から10mまでの5種類。1mと5mは0.1m単位、10mは1m単位で返った- 標高タイル(PNG)を自分で読むと、7地点でAPIと同じ値になった。51地点の断面はタイル12回で計算できた
- 試験公開のプログラムで、過度のアクセスは遮断されうる。多数の地点はタイルにし、出典を残す
ほかの公共データとの使い分けは公共データAPIのガイドにあります。地域コードで気象の情報を引く方法は気象庁の天気予報JSONとアメダスの観測値JSONにまとめています。
この記事の情報・検証メモ
- 公開日
- 情報確認
- 参考リンク
- 4件
- 更新性
- 定期更新
仕様・料金・提供範囲が変わりやすいテーマは、公開日・更新日・情報確認日を分けて管理します。 導入前には必ず記事末尾の一次情報と公式ドキュメントで最新状況を確認してください。
一次情報・参考リンク
- 地理院地図|サーバサイドで経緯度から標高を求めるプログラム(標高APIの説明) https://maps.gsi.go.jp/development/elevation_s.html
- 地理院地図|標高タイルの詳細仕様 https://maps.gsi.go.jp/development/demtile.html
- 地理院タイル一覧(標高タイル(基盤地図情報数値標高モデル)の項・利用条件) https://maps.gsi.go.jp/development/ichiran.html
- 国土地理院コンテンツ利用規約(令和7年11月20日改正) https://www.gsi.go.jp/kikakuchousei/kikakuchousei40182.html