1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

交通費精算のバス停名を手で書くのがつらいので、PASMO をタッチした瞬間に iPhone から位置情報を飛ばすことにした

1
Last updated at Posted at 2026-10-30

交通費精算のバス停名を手で書くのがつらいので、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 側: ウォレットのオートメーションで位置を送る

オートメーションを作る

ショートカットアプリの「オートメーション」タブから、「ウォレット」をきっかけにするオートメーションを作ります。カードを選んでおくと、そのカードで支払ったとき(改札・バス・物販のタッチ)に、指定したショートカットが動きます。

「すぐに実行」にしておくと、タッチのたびに確認なしで動きます。

ショートカットの中身

ショートカット「バスログ」のアクションは、おおむね次の順番です。

  1. 「テキスト」: 送信用のトークン
  2. 「もし」: 「ショートカットの入力」が空なら true、それ以外は false(手動で実行したテストかどうか)
  3. 「現在地を取得」: 精度は「最高」
  4. 「現在の日付」を「日付を書式設定」: ISO 8601、時刻を含める
  5. 「URL の内容を取得」: POST、ヘッダに Authorization: Bearer <トークン>、本文は JSON
  6. 手動で実行したときだけ、送信の結果を通知で表示する

送る 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 で受け取ります。説明のために簡略化したコードです。

app/api/log/route.ts
// 説明用に簡略化しています
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 それぞれについて記事にまとめています。

位置情報を預かるうえで気を付けたこと

自分のためだけなら気にしなくてよいことも、他の人の位置情報を預かるとなると話が変わります。主に次のようにしました。

  • タッチしたときだけ記録する(常に追跡しない)
  • 場所に関する列は、アプリケーション側で暗号化してから保存する(データベースの中身が漏れても、座標やバス停名が読めないように)
  • 本人も記録の中身は書き換えられない(勤務先などに出す明細として信用してもらうため。削除はできる)
  • 削除は物理削除(論理削除で残し続けない)

この辺りの考え方と、検討したうえでやらなかったことは、続きの記事に詳しく書きました。

サービスにしました

この仕組みを「バスログ」というサービスにして、テスト版を公開しています。メールアドレスまたは Google アカウントで、すぐに使い始められます。

  • 対応: iPhone の PASMO・Suica(Apple Pay)
  • 未対応: カード版(物理カード)と Android。タッチのときに iPhone が関わらないと、位置を記録するきっかけがないためです
  • 無料期間: 登録した月の 3 か月後の月末まで無料です。クレジットカードなどの登録は要らず、自動で料金が発生することもありません
  • 有料プラン: 通常料金は月額 500 円(税込)の予定です。2027 年 1 月 31 日までに登録した方は、半額(月額 250 円)でお使いいただけます(有料プランを解約して改めて契約する場合や、退会した場合は対象外です)

個人開発ですが、位置情報という大切なデータを預かるサービスなので、私が代表を務める会社(社員は私 1 人です)の名義で運営しています。運営者の情報はこちらにまとめています。

「うちの会社もバス停名を書かされる」という方に、試していただけるとうれしいです。

「PASMO」は株式会社パスモの登録商標です。「Suica」は東日本旅客鉄道株式会社の登録商標です。バスログは、これらの会社とは関係のない、独立したサービスです。

おわりに

「履歴に載らないなら、載せられる瞬間に自分で記録する」という、わりと力技の解決でした。それでも、ショートカットと PostGIS とオープンデータだけで、外部の有料 API を使わずに作れました。

ウォレットのオートメーションは、交通費の記録以外にも使い道がありそうです。ほかの使い方をご存じの方がいれば、コメントで教えてください。

続きの記事

出典

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?