Claudeで周遊モデルコースを作る — 営業時間と移動時間をAPIで照合
観光協会・DMOの周遊モデルコースを、Places APIの営業時間とRoutes APIの移動時間でClaude Codeに照合させる手順。日またぎ営業や定休日の落とし穴、課金の仕組み、APIキーの守り方までまとめます。
観光協会やDMOのモデルコースは、書いた時点では成立していても、定休日や営業時間の変更で崩れます。Claudeにコース案を作らせるだけなら数分で済みますが、「10時開館のはずが実際は10時半」といったずれは、人が地図アプリで一件ずつ確かめる作業になりがちです。そこで、コース案はClaudeに作らせ、成立するかどうかはAPIの値で機械的に判定する分担にします。この記事では、Places API(New)で施設の営業時間を取り、Routes APIで移動時間を取り、Claude Codeに照合スクリプトを書かせて回す手順を扱います。個人の旅行しおりをチャットで作る場合は、Claudeで旅行のしおりを作らせる手順が入口です。
Claudeに任せる部分とAPIに任せる部分を分ける
Claudeは、施設の候補出し、見どころの順序、所要時間の目安といった「案」を作るのが得意です。一方で営業時間や定休日は、Claudeの記憶やWeb検索の要約では古い値が混ざりえます。そこで営業時間は、施設の位置情報サービスから値を取って突き合わせます。
Google MapsのPlaces APIは、場所に関するHTTPリクエストを受け、整形済みの場所データを返すサービスです。取得できる詳細には営業時間が含まれます。場所はプレイスIDで一意に識別され、Text SearchやGeocoding、Routes、Address Validationなど複数のAPIから入手できます。
役割は次のように分けます。
周遊コース作りの分担
Claudeが担う
施設候補の洗い出し、巡る順序の案、滞在時間の仮置き、照合スクリプトの作成。
Places APIが担う
施設の実在確認、営業状況、定休日を含む週間の営業時間、座標。
Routes APIが担う
施設間の移動時間。車のルートでは交通状況を加味する指定もできます。
人が担う
照合を通ったコースの現地確認と、観光協会としての最終判断。
照合で落ちたコースは捨てるのではなく、どの施設のどの時刻が合わなかったかをClaudeに戻し、順序を組み替えさせます。この往復が、一覧表を手で直す作業の代わりになります。
事前に決めておくこと
作業に入る前に、次の3点を決めます。
- APIキー: Google Cloudで請求先を設定し、Places API(New)を有効にします。ルート計算も使うなら、Routes APIも有効にします。キーは環境変数に置き、スクリプトには書きません
- 対象地域の範囲: 検索は「市名 + 施設の種類」で絞ります。距離で絞るなら位置バイアスを使います
- コースの条件: 出発地、開始時刻、日付(曜日)、各施設の滞在時間を表にしておきます
曜日は特に重要です。営業時間は曜日ごとに違うので、「土曜日の9時半スタート」のように日付を固定しないと、照合の結果に意味がありません。
営業時間を取る — Text Searchとフィールドマスク
施設名から営業時間を取る入口は、Text Search(New)です。HTTPのPOSTでplaces:searchTextを呼び、本文にtextQueryを渡します。取得したい項目は、ヘッダーX-Goog-FieldMaskで指定します。
curl -X POST 'https://places.googleapis.com/v1/places:searchText' \
-H 'Content-Type: application/json' \
-H "X-Goog-Api-Key: $MAPS_API_KEY" \
-H 'X-Goog-FieldMask: places.id,places.displayName,places.location,\
places.businessStatus,places.regularOpeningHours,places.utcOffsetMinutes' \
-d '{"textQuery": "城下町資料館 観光協会の所在市名", "languageCode": "ja", "pageSize": 3}'textQueryの中身は、実際には対象の市名と施設名に置き換えます。ここで押さえる点は3つあります。
- フィールドマスクは必須です。Text Searchは、少なくとも1つの項目を指定しないとエラーを返します。既定の返却項目はありません
languageCodeは明示します。省略すると英語になるため、日本語の施設名や住所が欲しいならjaを渡します。無効な言語コードはエラーになりますpageSizeは最大20件です。未指定でも1ページ20件が上限なので、「候補を3件だけ見たい」ときは小さく指定します
営業時間の項目は課金区分が上がる
Placesの料金は、リクエストに含めた項目の課金区分(SKU)で決まります。複数の区分の項目を混ぜたときは、最も高い区分で請求されます。営業時間に関するregularOpeningHoursとcurrentOpeningHoursは、Text SearchではEnterpriseの区分です。displayNameやbusinessStatus、utcOffsetMinutesはPro、locationもText SearchではPro、という分け方です。
つまり、営業時間を1項目でも含めると、そのリクエスト全体がEnterprise扱いになります。施設を探す段階ではIDだけを取り、コース候補が絞れてから営業時間を取る、という二段構えにすると無駄な課金を減らせます。単価は改定されるので、見積もりは料金ページの最新値で確かめてください。
営業時間の読み方 — 日またぎ・定休日・特別営業日
営業時間の項目はOpeningHoursというオブジェクトで返ります。コース照合で使う主な項目は次のとおりです。
| 項目 | 内容 | 照合での使いどころ |
|---|---|---|
periods | 内容開く時刻と閉まる時刻の組。現地時間 | 照合での使いどころ曜日・時刻の判定の本体 |
weekdayDescriptions | 内容曜日ごとの表示用文字列 | 照合での使いどころ人が確認するときの表示 |
specialDays | 内容祝日などの特別営業日 | 照合での使いどころ現在の営業時間にだけ入る |
nextOpenTime / nextCloseTime | 内容次に開く・閉まる時刻 | 照合での使いどころ7日先までの次の開閉の目安。週単位の照合にはperiodsが要る |
openNow | 内容今この瞬間に営業中か | 照合での使いどころ将来のコース照合には使えない |
openNowは「リクエストした瞬間」の状態なので、来週土曜日のコースの判定には使えません。コース照合の主役はperiodsです。
periodsを扱うときの落とし穴
periodsの各要素は、openとcloseの組です。それぞれが曜日(0が日曜、1が月曜)、時、分を持ちます。次の点を間違えると、照合がエラーを出さずに誤った結果を返します。
- 配列の先頭が日曜とは限りません。開始曜日はAPIが決めるため、添字ではなく
dayの値で読みます weekdayDescriptionsの並びとperiodsの並びは独立しています。両者を同じ添字で突き合わせてはいけません- 日またぎ営業があります。日曜18時に開き、月曜2時に閉まる店では、
closeの曜日が翌日になります - 24時間営業は
closeが設定されません。開始が日曜0時0分のopenだけが返ります periodsが空の配列は「常に閉まっている」状態で、改装による臨時休業などで起こります。項目自体が無い場合とは区別します
regularOpeningHoursは通常の週間予定、currentOpeningHoursは今日から7日間の予定で、後者にはspecialDaysが入ります。祝日や年末年始が絡むコースは、currentOpeningHoursの対象期間(リクエスト日から7日間)に入るときだけ特別営業日を確認できる点に注意します。
閉店・休業の施設を落とす
businessStatusにはCLOSED_TEMPORARILYのような値があります。営業時間が取れても、事業そのものが休止中なら、そのコースは成立しません。営業状況を最初に見て、営業中以外は候補から外します。
移動時間を取る — Routes APIのcomputeRoutes
施設間の移動時間は、Routes APIのcomputeRoutesで取ります。HTTPのPOSTでhttps://routes.googleapis.com/directions/v2:computeRoutesを呼び、出発地と目的地、移動手段(travelMode)を本文に渡します。ここでも、返却項目はフィールドマスクで指定します。まず車の例です。
curl -X POST 'https://routes.googleapis.com/directions/v2:computeRoutes' \
-H 'Content-Type: application/json' \
-H "X-Goog-Api-Key: $MAPS_API_KEY" \
-H 'X-Goog-FieldMask: routes.duration,routes.distanceMeters' \
-d '{
"origin": {"location": {"latLng": {"latitude": 35.0, "longitude": 135.0}}},
"destination": {"location": {"latLng": {"latitude": 35.01, "longitude": 135.01}}},
"travelMode": "DRIVE",
"routingPreference": "TRAFFIC_AWARE"
}'座標は例のダミー値で、実際にはText Searchで得たlocationを渡します。応答のroutes.durationは、"165s"のように秒数にsが付いた文字列で返ります。数値として使う前に、末尾のsを外して整数に直す処理が要ります。
徒歩・公共交通のコースはtravelModeと時刻を変える
周遊コースでは徒歩や電車・バスで回る行程も多いため、travelModeの値を使い分けます。選べる値は次のとおりです。
travelMode | 移動手段 | 照合で気をつける点 |
|---|---|---|
DRIVE | 移動手段車(未指定のときの既定) | 照合で気をつける点routingPreferenceを指定できる |
WALK | 移動手段徒歩 | 照合で気をつける点ベータ提供。歩道や歩行者用の経路が不完全なことがある |
BICYCLE | 移動手段自転車 | 照合で気をつける点ベータ提供。自転車道が不完全なことがある |
TWO_WHEELER | 移動手段バイクなど原動機付きの二輪 | 照合で気をつける点ベータ提供。routingPreferenceを指定できる |
TRANSIT | 移動手段公共交通 | 照合で気をつける点transitPreferencesとarrivalTimeを指定できる |
routingPreference(TRAFFIC_AWAREなど)を指定できるのはDRIVEとTWO_WHEELERのときだけで、WALKやTRANSITに付けるとリクエストが失敗します。徒歩と公共交通の例では、この項目を外します。WALK・BICYCLE・TWO_WHEELERの経路をアプリに表示する場合は、経路が不完全な場合がある旨の警告を利用者に見せる必要があります。
将来の日付のコースを照合するときは、departureTimeにコース開始時刻を渡します。省略するとリクエストした時刻が使われるので、来週土曜の電車の所要時間を知りたいのに「今」の時刻表で計算してしまいます。形式はRFC 3339で、+09:00のようなオフセット付きでも渡せます。
curl -X POST 'https://routes.googleapis.com/directions/v2:computeRoutes' \
-H 'Content-Type: application/json' \
-H "X-Goog-Api-Key: $MAPS_API_KEY" \
-H 'X-Goog-FieldMask: routes.duration,routes.distanceMeters' \
-d '{
"origin": {"location": {"latLng": {"latitude": 35.0, "longitude": 135.0}}},
"destination": {"location": {"latLng": {"latitude": 35.01, "longitude": 135.01}}},
"travelMode": "TRANSIT",
"departureTime": "2026-11-07T09:30:00+09:00"
}'departureTimeを過去にできるのはTRANSITのときだけです。公共交通の経路が引けるのは過去7日から未来100日までなので、半年先のシーズンのコースは、日が近づいてから取り直します。arrivalTimeはTRANSIT以外では無視され、departureTimeとの同時指定はできません。
Claude Codeに照合スクリプトを書かせる
ここからがClaudeの出番です。取得した営業時間と移動時間を、コース案と突き合わせるスクリプトを書かせます。指示は、判定の仕様を言葉で渡すのがコツです。
周遊コース案(施設名と滞在分数の配列)と曜日・開始時刻を受け取り、
各施設に着く時刻と退出時刻を順に計算するPythonスクリプトを書いて。
- 営業時間はPlaces APIのperiodsを使い、日またぎと24時間営業を扱う
- 施設間の移動秒数はRoutes APIのdurationを使う
- 滞在の全時間帯が営業時間に収まらない施設があれば、その施設名と時刻を出して失敗にする
- APIキーは環境変数MAPS_API_KEYから読み、ファイルに書かない判定の中核は次のような関数になります。以下はperiodsの仕様に沿って書いた例で、日またぎと24時間営業を扱います。
WEEK = 7 * 1440
def to_min(point):
return point["day"] * 1440 + point["hour"] * 60 + point["minute"]
def is_open(hours, day, start, end):
"""day: 0=日曜。start/end は当日0時からの分。"""
s = day * 1440 + start
e = day * 1440 + end
for p in hours.get("periods", []):
if "close" not in p: # 24時間営業
return True
o, c = to_min(p["open"]), to_min(p["close"])
if c <= o: # 日またぎ
c += WEEK
for shift in (0, WEEK):
if o <= s + shift and e + shift <= c:
return True
return Falseこの関数は、月曜9時〜17時の施設に「9時半から1時間」の滞在を入れると真、月曜10時〜12時の施設に「11時15分から1時間」を入れると偽、日曜18時〜月曜2時の店に月曜0時台の滞在を入れると真、と返す想定です。実際に書かせたコードは、必ずこのような境界の例で自分でも確かめます。
検証は「失敗するケース」から先に回す
Claude Codeにスクリプトを書かせたら、成功例より先に、落ちるべきケースを作らせて回します。
- 開館前に着く順序のコースで、確かに失敗になること
- 日またぎの店に、深夜の滞在時間帯を入れて通ること
- 定休日(その曜日の
periodsが無い施設)を含むコースで失敗になること
これらはAPIの応答を保存した小さなJSONで再現でき、APIを呼ばなくても確かめられます。コース全体の組み替えは、失敗の出力(施設名・到着時刻・営業時間)をそのままClaudeに戻して依頼します。
APIキーを守る権限設定
Claude Codeにスクリプトを書かせて実行させると、環境変数のキーを使ったコマンドが走ります。キーを書いたファイルを読ませない設定も入れておきます。Claude Codeの権限ルールは、拒否、確認、許可の順に評価され、拒否が最優先です。Read(./.env)のようにファイルを指定した読み取り拒否が書けます。設定の書き方と効く範囲は、.envを読ませないRead denyの書き方で扱っています。
.claude/settings.jsonには、次のような拒否ルールを置けます。
{
"permissions": {
"deny": ["Read(./.env)"]
}
}Bashの拒否ルールは、Claudeが通常書く形のコマンドしか止められない点にも注意が要ります。権限ルールが止めるのはClaude Codeの操作だけです。キーをコードやリポジトリに残さない運用は別に必要です。
観光協会の運用に落とし込むときの要点
コース照合を一度きりの作業にせず、更新のたびに回せる形にすると効果が出ます。
シーズン前の更新作業の流れ
- 1
施設リストを固定する
モデルコースに載せている施設を、名称とプレイスIDの対応表にしておきます。毎回の検索結果は揺れるので、IDで引きます。
- 2
営業時間を取り直す
コースの対象期間に近い時点で、各施設の営業時間と営業状況を取得します。
- 3
全コースを照合する
照合スクリプトで、各コースの各曜日を判定し、失敗したコースと施設を一覧にします。
- 4
失敗分だけ直す
失敗の理由をClaudeに渡し、順序の入れ替えや滞在時間の短縮案を作らせます。最後は人が現地の事情を踏まえて決めます。
APIの営業時間は、リクエスト時点でGoogleが持つ値です。臨時休業や行事による変更がその後に起きる可能性もあるため、照合を通ったコースでも、公開前に施設へ直接確認する工程は残します。Claudeの役割は、確認する前に「そもそも成立しない案」を落としておくことです。
まとめ
周遊モデルコースは、案づくりをClaude、成立判定をAPIの値に分けると、更新のたびに全コースを機械的に確かめられます。営業時間はperiodsの日またぎと24時間営業、課金はEnterprise区分への上昇、移動時間は秒数文字列の変換が、つまずきやすい3点です。まず1コースと3施設で照合を回し、失敗するケースが正しく失敗することを確かめてから、全コースへ広げる順番が確実です。