交通費精算のバス停名を手で書くのがつらいので、PASMO をタッチした瞬間に iPhone から位置情報を飛ばすことにした
はじめに
勤務先などに交通費を精算するとき、バスの「乗車バス停・降車バス停」を書くよう求められたことはありませんか。
電車なら交通系 IC の利用履歴に駅名が残りますが、定額運賃のバスは、履歴にバス停名が載りません。私は毎月、カレンダーと記憶をたどりながらバス停名を書いていました。
そこで、PASMO・Suica をタッチした瞬間に、iPhone のショートカットで位置情報をサーバーに送り、月末に利用明細 PDF と突き合わせて、バス停名付きの明細を作る仕組みを作りました。
この記事で分かること:
- iPhone の「ウォレット」のオートメーションで、交通系 IC のタッチをきっかけにショートカットを動かす方法と、そのハマりどころ
- Next.js で受け取った座標から、PostGIS と国土数値情報で近くのバス停を引く方法
- 時刻が載っていない利用明細 PDF と、位置情報のログを突き合わせる考え方
困りごと: 利用履歴にバス停名が載らない
PASMO の公式サイトによると、利用履歴(残額履歴)に載るのは、利用日・種別(「入」「出」「バス等」「物販」など)・鉄道の駅名・バス事業者名・残額などです。利用した時刻と、バス停名は載りません。
実際にダウンロードした利用明細 PDF でも、バスの行は次のようになっています(例)。
| 月 | 日 | 種別 | 利用駅 | 残額 | 差額 |
|---|---|---|---|---|---|
| 10 | 22 | 入 新宿 出 中野 | 2,580 | -200 | |
| 10 | 22 | バス等 | 〇〇バス | 2,370 | -210 |
「どの会社のバスに乗ったか」は分かりますが、「どこから乗ったか」は分かりません。同じ日に何度か乗ると、どの行がどの便なのかも分からなくなります。
詳しい背景と、手書きのメモなどバスログを使わない方法の比較は、別の記事にまとめています。
方針: タッチした瞬間の位置を残し、月末に明細と突き合わせる
全体の流れは次のとおりです。
決めたことは 2 つです。
- 常に位置を追跡せず、タッチしたときだけ記録する。交通費の精算に要らない移動まで残したくないためです。スマホのバッテリーにもやさしくなります。
- 外部の有料 API を使わない。バス停のデータは国土数値情報(オープンデータ)を使い、近くのバス停の検索は PostGIS で行います。
iPhone 側: ウォレットのオートメーションで位置を送る
オートメーションを作る
ショートカットアプリの「オートメーション」タブから、「ウォレット」をきっかけにするオートメーションを作ります。カードを選んでおくと、そのカードで支払ったとき(改札・バス・物販のタッチ)に、指定したショートカットが動きます。
「すぐに実行」にしておくと、タッチのたびに確認なしで動きます。
ショートカットの中身
ショートカット「バスログ」のアクションは、おおむね次の順番です。
- 「テキスト」: 送信用のトークン
- 「もし」: 「ショートカットの入力」が空なら
true、それ以外はfalse(手動で実行したテストかどうか) - 「現在地を取得」: 精度は「最高」
- 「現在の日付」を「日付を書式設定」: ISO 8601、時刻を含める
- 「URL の内容を取得」:
POST、ヘッダにAuthorization: Bearer <トークン>、本文は JSON - 手動で実行したときだけ、送信の結果を通知で表示する
送る JSON はこのような形です(値は例です)。
{
"paid_at": "2026-10-22T08:31:12+09:00",
"latitude": 35.7058,
"longitude": 139.6658,
"test": "false",
"transaction": "記名PASMO",
"shortcut_version": "1"
}
ハマりどころ
作ってみると、思っていたとおりには動かない点がいくつかありました。
1. 金額が取れない
ウォレットのオートメーションからは取引の金額を取り出せるはずでした。しかし、PASMO・Suica の決済では金額が空で届きます。金額は、月末に取り込む利用明細 PDF の「差額」を使うことにしました。
2. ライブラリのショートカットでは、入力を「取引」として扱えない
オートメーションの中に直接アクションを並べると、取引の項目(名前・カードなど)を取り出せます。ところが、それでは配布できません。オートメーションは共有も複製もできないためです。
配布できるのはライブラリのショートカットです。ただ、ライブラリのショートカットでは、受け取った入力を「取引」として扱えません。そこで、入力をそのまま transaction として送ることにしました。中身にはウォレットでのカードの名前(「記名PASMO」「My Suica」など)が入るので、どのカードのタッチかはサーバー側で判定しています。
3. オートメーションは配れない。トークンは「読み込み時の質問」で
ショートカット本体は iCloud リンクで配布できます。一方、オートメーションは各自で作ってもらう必要があります。
送信用のトークンは利用者ごとに違うため、先頭の「テキスト」アクションに「読み込み時の質問」を付けました。ショートカットを追加するときに、トークンを貼り付けてもらう形です。共有する前に、リンクの中に自分のトークンが残っていないかを必ず確かめてください(私は、自分用に作ったショートカットの複製を作ってトークン部分のテキストを空にしてから共有しています)。
4. 設定の途中で失敗に気付けるようにする
タッチのたびに通知が出るとわずらわしいため、通知は出しません。ただ、それではトークンを貼り間違えても気付けません。
そこで、手動で実行したとき(ショートカットの入力が空のとき)はテスト送信として扱い、「バスログに送信しました」または「送信できませんでした: 〜」と通知を出すようにしました。テスト送信は、月末の突き合わせには使いません。
5. 位置情報の許可
位置情報は「App の使用中は許可」、ショートカットからの送信の確認は「常に許可」を選びます。「1 度だけ許可」を選ぶと、次のタッチのときに止まってしまいます。
サーバー側: Next.js で受けて、PostGIS で近くのバス停を引く
受け取る API
Next.js(App Router)の Route Handler で受け取ります。説明のために簡略化したコードです。
// 説明用に簡略化しています
export async function POST(request: Request) {
const userId = await findUserByToken(request.headers.get("authorization"));
if (!userId) {
return Response.json({ error: "認証に失敗しました" }, { status: 401 });
}
const body = await request.json().catch(() => null);
const latitude = toNumber(body?.latitude);
const longitude = toNumber(body?.longitude);
// 近くのバス停を探す(見つからなければ null)
const busStop =
latitude !== null && longitude !== null
? await findNearestBusStop(latitude, longitude)
: null;
// 形式が想定外でも、いったん保存する(読み取れなかった項目は null にして理由を残す)
await saveTripLog({ userId, body, latitude, longitude, busStop });
return Response.json({ ok: true });
}
ショートカットは iOS の更新などで送る中身が変わることがあります。実際、最初のころは項目名のスペルを間違えて paied_at で送っていました。そのため、形式が想定外でも捨てずに保存し、後から直せるようにしています。
国土数値情報のバス停データ
バス停の位置は、国土交通省の「国土数値情報 バス停留所データ(P11)」を使います。全国のバス停の名前・事業者・座標が、CC BY 4.0 で公開されています。これを PostGIS の geography 型の列を持つ表に取り込みます。
create table bus_stops_master (
id bigint generated always as identity primary key,
name text not null, -- 停留所名
operator text, -- バス事業者名
latitude double precision not null,
longitude double precision not null,
location geography(Point, 4326) generated always as (
st_setsrid(st_makepoint(longitude, latitude), 4326)::geography
) stored
);
create index on bus_stops_master using gist (location);
近くのバス停を引く
ST_DWithin で半径の中に絞ってから、距離の近い順に 1 件を取ります。geography 型なので、距離はメートルで扱えます。
create function find_nearest_bus_stop(
lat double precision,
lng double precision,
radius_m double precision
)
returns table (id bigint, name text, operator text, distance_m double precision)
language sql
stable
as $$
with p as (
select st_setsrid(st_makepoint(lng, lat), 4326)::geography as g
)
select s.id, s.name, s.operator, st_distance(s.location, p.g) as distance_m
from bus_stops_master s, p
where st_dwithin(s.location, p.g, radius_m)
order by distance_m
limit 1;
$$;
ST_DWithin は GiST インデックスを使えるので、全国分のバス停が入っていても速く引けます。
ハマりどころ: 駅前のバスターミナル
使い始めてすぐ、駅前のバスターミナルで「バス停不明」になることがありました。
国土数値情報では、1 つのバス停は 1 つの点です。一方、駅前のターミナルは乗り場が広く、乗り場によってはその点からかなり離れます。実際に、点から 64m 離れた乗り場でタッチした記録がありました。
そこで、判定の半径を広げました。あわせて、近くに名前の違うバス停が複数あるとき(交差点の両側など)は、その候補の中からだけ本人が選び直せるようにしています。自由に書き換えられると明細として信用されないため、自動で決めたバス停も残し、選び直したことは画面と帳票に表示します。
月末: 時刻のない PDF とどう突き合わせるか
ここがいちばん悩んだところです。
モバイル PASMO の会員メニューサイトでは、利用明細を CSV では取得できず、PDF だけです。そして、PDF には時刻がありません。位置情報のログには秒単位の時刻があるのに、明細の側には日付しかないので、時刻で照らし合わせることができません。
そこで、同じ日の中の「利用の順番」で対応付けることにしました。明細の行は利用した順に並んでいて、ログも時刻順に並べられます。「バス等」の行に対応付いたログの位置から引いたバス停を、乗車バス停として明細に載せます。
ログが足りない日(タッチしたのにショートカットが動かなかった、など)は、対応付けられない行が出ます。そうした日は「要確認」として、理由を表示します。
この仕組みのため、明細から行を減らした PDF(一部の履歴だけを選んで印刷したものなど)では、順番がずれて正しく突き合わせられません。PDF は、期間内のすべての履歴を含めてダウンロードする必要があります。
PDF のダウンロード方法は、PASMO・Suica それぞれについて記事にまとめています。
- https://buslog.rightpath.jp/guide/mobile-pasmo-statement-pdf
- https://buslog.rightpath.jp/guide/mobile-suica-statement-pdf
位置情報を預かるうえで気を付けたこと
自分のためだけなら気にしなくてよいことも、他の人の位置情報を預かるとなると話が変わります。主に次のようにしました。
- タッチしたときだけ記録する(常に追跡しない)
- 場所に関する列は、アプリケーション側で暗号化してから保存する(データベースの中身が漏れても、座標やバス停名が読めないように)
- 本人も記録の中身は書き換えられない(勤務先などに出す明細として信用してもらうため。削除はできる)
- 削除は物理削除(論理削除で残し続けない)
この辺りの考え方と、検討したうえでやらなかったことは、続きの記事に詳しく書きました。
サービスにしました
この仕組みを「バスログ」というサービスにして、テスト版を公開しています。メールアドレスまたは Google アカウントで、すぐに使い始められます。
- 対応: iPhone の PASMO・Suica(Apple Pay)
- 未対応: カード版(物理カード)と Android。タッチのときに iPhone が関わらないと、位置を記録するきっかけがないためです
- 無料期間: 登録した月の 3 か月後の月末まで無料です。クレジットカードなどの登録は要らず、自動で料金が発生することもありません
- 有料プラン: 通常料金は月額 500 円(税込)の予定です。2027 年 1 月 31 日までに登録した方は、半額(月額 250 円)でお使いいただけます(有料プランを解約して改めて契約する場合や、退会した場合は対象外です)
個人開発ですが、位置情報という大切なデータを預かるサービスなので、私が代表を務める会社(社員は私 1 人です)の名義で運営しています。運営者の情報はこちらにまとめています。
「うちの会社もバス停名を書かされる」という方に、試していただけるとうれしいです。
「PASMO」は株式会社パスモの登録商標です。「Suica」は東日本旅客鉄道株式会社の登録商標です。バスログは、これらの会社とは関係のない、独立したサービスです。
おわりに
「履歴に載らないなら、載せられる瞬間に自分で記録する」という、わりと力技の解決でした。それでも、ショートカットと PostGIS とオープンデータだけで、外部の有料 API を使わずに作れました。
ウォレットのオートメーションは、交通費の記録以外にも使い道がありそうです。ほかの使い方をご存じの方がいれば、コメントで教えてください。
続きの記事
出典
- 国土数値情報(バス停留所データ)国土交通省(CC BY 4.0)を加工して作成
- PASMO 公式サイト「残額履歴について」https://www.pasmo.co.jp/about/service/history/