本文へスキップ

国土地理院の標高APIの使い方:「-----」の返り方、hsrcの精度、大量の地点は標高タイルで

SHAYOUWORLD 公開 約7分

要点

  1. URLは1本、キーは不要。 https://cyberjapandata2.gsi.go.jp/general/dem/scripts/getelevation.php?lon=経度&lat=緯度&outtype=JSON をGETすると、elevation と hsrc が返ります。
  2. 失敗もHTTP 200。 海上・国外・緯度と経度の取り違え・lon の省略は、どれも文字列の "-----" でした。成功時の elevation は数値です。
  3. hsrc を値と一緒に残す。 1m(レーザ)から10mまで5種類あり、桁数も違います。同じ断面の中でも地点ごとに変わりました。
  4. 大量の地点は標高タイル。 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の関数名)のどちらか一方を付けます。東京駅の丸の内駅前広場付近で呼び出しました。

Terminal window
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地点と失敗の返り方

地点緯度, 経度elevationhsrc応答時間
国土地理院(説明ページの使用例の座標)36.103543, 140.0853125.25m(レーザ)247ms
富士山 剣ヶ峰付近35.360625, 138.7273633774.81m(レーザ)48ms
東京駅 丸の内駅前広場付近35.681236, 139.7671253.61m(レーザ)48ms
江戸川区 船堀二丁目35.6838, 139.8573-0.81m(レーザ)68ms
琵琶湖の湖上35.25, 136.088510m184ms
旭岳付近43.6634, 142.85442286.95m(レーザ)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 200
lon を省く(lat=35.681236 のみ) -> {"elevation":"-----","hsrc":"-----"} HTTP 200

読み取れることは3つです。

  • 型で判定する。 成功時の elevation は数値、失敗時は文字列です。Number("-----") は NaN なので、そのまま計算に入れると後で気づきにくい壊れ方をします。
  • 入力の誤りと「データがない」が区別できない。 海上も、緯度経度の取り違えも、同じ応答です。呼ぶ前に、緯度経度が日本の範囲に入っているかを自分で確かめます。
  • 負の値は正常な値。 江戸川区の地点は -0.8 でした。0未満を異常値として捨てると、海抜ゼロメートル地帯の値を失います。

hsrc の種類と桁数

説明ページは、hsrc を精度の高い順に次の5つとしています。その地点で最も精度の高いデータの値を返すとあります。

hsrc実測で返った例返った桁
1m(レーザ)富士山 3774.8、東京駅 3.6、江戸川区 -0.80.1m
5m(レーザ)国土地理院 25.2、旭岳 2286.90.1m
5m(写真測量)今回の8地点にはなかった—
5m(写真測量5C)今回の8地点にはなかった—
10m琵琶湖の湖上 851m

説明ページの表には、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.24dem5a_png(dem1a_png は404)25.2
富士山 剣ヶ峰付近3774.84dem1a_png3774.8
東京駅 丸の内駅前広場付近3.60dem1a_png3.6
江戸川区 船堀二丁目-0.79dem1a_png-0.8
琵琶湖の湖上84.60dem_png(1m・5mの4つは404)85
旭岳付近2286.89dem5a_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地点に分けて、タイルから標高を計算しました。

51地点
計算した地点
約23mおき
12回
タイルの取得
同じタイルは再取得しない。404の4回を含む
45 / 6
hsrc の内訳
1m(レーザ)45地点、10m 6地点
2.05〜7.77m
標高の範囲
始点3.6m、終点7.77m
dem-tile.mjs で計算(2026-10-20)。終点はAPIでも確認し、8(10m)が返った

同じ処理を標高APIで行うと51回の呼び出しになります。タイルは1枚で256×256画素を持つので、近い地点が並ぶ断面や面の計算では、取得回数が地点数よりずっと少なくなります。

もう一つ分かったのは、1本の断面の中でも hsrc が変わることです。51地点のうち6地点は、DEM1AとDEM5Aのタイルの該当画素が無効値で、10mのデータの値になりました。終点は標高APIでも確かめ、{"elevation":8,"hsrc":"10m"}(タイルの値7.77を1m単位にした値)が返りました。断面図を描くときは、地点ごとに hsrc を残し、データの種類が変わる区間を区別して表示します。

AIエージェント・MCPツールにするときの設計

  1. 呼ぶ前に入力を確かめる。 緯度と経度の取り違えは "-----" になり、エラーとしては返りません。日本の範囲を大まかに外れる値や、緯度に100を超える値が入っていたら、APIを呼ばずに止めます。
  2. 失敗を null と理由に変換する。 elevation が数値でなければ { elevation: null, reason: "no_data" } のように返し、0や前の地点の値で埋めません。
  3. hsrc と取得元を返す。 値と一緒に hsrc、取得元URL、取得日時を返します。出典の型はMCPの出典情報を欠落させないoutputSchema設計に合わせられます。
  4. 多数の地点はタイルにし、間隔はサーバー側で決める。 1点ずつAPIを呼ぶのは少数の問い合わせに限ります。上限や待機をモデルに任せない理由は公共データAPIをMCP化する設計パターンに書きました。
  5. 標高を安全の判断に使わせない。 返る値は数値標高モデルの値で、建物の上の高さや浸水の危険度ではありません。避難や浸水の判断が必要な質問には、自治体のハザードマップなどの公式の情報を確認するよう返します。
- 標高の回答は 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にまとめています。

この記事の情報・検証メモ
Tags
公開日
情報確認
参考リンク
4件
更新性
定期更新
更新管理

仕様・料金・提供範囲が変わりやすいテーマは、公開日・更新日・情報確認日を分けて管理します。 導入前には必ず記事末尾の一次情報と公式ドキュメントで最新状況を確認してください。

検証メモ
Node.js 24.8.0(組み込み fetch と zlib、依存なし)、curl(Git Bash) 国土地理院 標高API・標高タイル・逆ジオコーダー 実行日 2026-10-20(計47リクエスト)
図解を保存・共有

記事の要点を1枚にまとめました。画像は新しいタブで開いて保存できます。

国土地理院の標高APIの使い方:「-----」の返り方、hsrcの精度、大量の地点は標高タイルで 失敗もHTTP 200で返る。値が数値か「-----」かを先に見て、hsrcを値と一緒に残す 標高APIの返り方:成功は数値、失敗は文字列の「-----」。海上・国外・緯度経度の取り違え・lon省略も「-----」。江戸川区の地点は -0.8。負の値も正常な値。 hsrcと精度:1m(レーザ)、5m(レーザ)、5m(写真測量)、5m(写真測量5C)、10m。1m・5mは0.1m、10mは1m単位で返った。1本の断面でも地点ごとにhsrcが変わる。 大量の地点は標高タイル:PNGの画素から 2^16R + 2^8G + B で計算。7地点でAPIの値と一致した。51地点の断面がタイル12回で済んだ。
国土地理院の標高APIの使い方:「-----」の返り方、hsrcの精度、大量の地点は標高タイルで 記事の要約 2026.10.20 入門・導入ガイド
Primary sources

一次情報・参考リンク

About the author
SHAYOUWORLD

日本の公共データAPIを使うMCPサーバーを作って公開し、ローカルLLMを自分のGPUで測った記録を、一次情報・検証ログ・失敗例とあわせて整理します。