Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Claude AI Assistant Integration

このドキュメントは、jp-postal-code-api プロジェクトでの Claude AI との協力・開発に関する情報をまとめています。

## プロジェクト概要

jp-postal-code-api は、日本の郵便番号データを提供するAPIプロジェクトです。日本郵便が提供するCSVデータを解析し、JSON形式のAPIとして提供します。

## Claude との協力内容

### 開発支援
- コードレビューとリファクタリングの提案
- PHPコードの品質向上
- テストコードの改善
- ドキュメントの整備

### 自動化タスク
- GitHub Actions ワークフローの最適化
- データ処理ロジックの改善
- エラーハンドリングの強化

## プロジェクト構造

```
src/
├── JpPostalCodeApi.php # メインAPIクラス
├── Command/ # コマンドライン処理
├── Csv/ # CSV解析処理
├── DataSource/ # データソース管理
├── Model/ # データモデル
└── Util/ # ユーティリティ
```

## 重要なファイル

- `bin/console build`: データビルドコマンド
- `.github/workflows/cron.yaml`: 定期実行ワークフロー
- `composer.json`: 依存関係定義
- `phpunit.xml.dist`: テスト設定

## 開発ガイドライン

### コーディング規約
- PSR-4 オートローディング
- PSR-12 コーディングスタイル
- PHPStan レベル9 での静的解析
- PHPUnit でのテストカバレッジ

### データ処理
- 日本郵便のKEN_ALLデータを使用
- 事業所個別郵便番号データにも対応
- ローマ字データの処理

## CI/CD

GitHub Actions による自動化:
- 毎日00:00 JSTでデータ更新
- コードの品質チェック
- 自動テスト実行
- APIドキュメント生成

## API仕様

### エンドポイント
- `GET /api/v1/{postal_code}.json`
- レスポンス形式:JSON
- 文字エンコーディング:UTF-8

### レスポンス例
```json
{
"data": [
{
"postal_code": "1000001",
"prefecture": "東京都",
"city": "千代田区",
"town": "千代田"
}
]
}
```

## 今後の改善点

- [ ] API レスポンスの最適化
- [ ] データ更新頻度の調整
- [ ] エラーハンドリングの強化
- [ ] パフォーマンスの向上
- [ ] ドキュメントの充実

## 連絡先

プロジェクトに関する質問や提案は、GitHub Issues をご利用ください。

---

*このドキュメントは Claude AI アシスタントとの協力により作成・更新されています。*
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,14 @@ Web APIの配信データは [日本郵便によって公開されているデ
https://{あなたのGitHubユーザー名}.github.io/jp-postal-code-api/api/v1/{郵便番号}.json
```


のようになります。

```
https://nishiokya.github.io/jp-postal-code-api/api/v1/441-0841.json

```

ただし、それでも悪意ある攻撃者によって大量のリクエストが行われると利用制限の対象になる可能性があります。どうしても心配な場合は、フォークしたリポジトリを [Cloudflare Pages](https://www.cloudflare.com/ja-jp/developer-platform/pages/) などの多機能なホスティングサービスやその他PaaSなどに接続して、BASIC認証などをかけた状態でWeb APIをホストするといった運用を検討してください。

## ローカル環境での使用
Expand Down
4 changes: 3 additions & 1 deletion bin/console
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ use Ttskch\JpPostalCodeApi\Csv\KenAll\CsvParser as KenAllCsvParser;
use Ttskch\JpPostalCodeApi\Csv\KenAllRome\CsvParser as KenAllRomeCsvParser;
use Ttskch\JpPostalCodeApi\DataSource\CsvProvider;
use Ttskch\JpPostalCodeApi\FileSystem\BaseDirectory;
use Ttskch\JpPostalCodeApi\Suggest\SuggestIndexBuilder;
use Ttskch\JpPostalCodeApi\JpPostalCodeApi;

(new JpPostalCodeApi(
new CsvProvider(),
new KenAllCsvParser(),
new KenAllRomeCsvParser(),
new JigyosyoCsvParser(),
new BaseDirectory()
new BaseDirectory(),
new SuggestIndexBuilder()
))->run();
146,908 changes: 146,908 additions & 0 deletions docs/api/v1/suggest/zip7.jsonl

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
<script>
// ===== 設定 =====
const BASE = 'https://nishiokya.github.io/jp-postal-code-api/api/v1/'; // APIのベース
const SUGGEST_URL = './api/v1/zip_suggest_digit3.txt'; // 3桁候補ファイルのパスを合わせてください
const SUGGEST_URL = 'https://nishiokya.github.io/jp-postal-code-api/api/v1/suggest/zip3.txt'; // 3桁候補ファイル

// ===== DOM =====
const $form = document.getElementById('form');
Expand Down Expand Up @@ -155,7 +155,7 @@
placeHolder: "3桁(例:100)",
data: { src: zip3List, cache: true },
threshold: 0,
searchEngine: "loose",
searchEngine: "strict",
resultsList: { maxResults: 20, tabSelect: true },
resultItem: { highlight: true }
});
Expand Down
12 changes: 12 additions & 0 deletions src/Command/BuildCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Ttskch\JpPostalCodeApi\DataSource\CsvProviderInterface;
use Ttskch\JpPostalCodeApi\DataSource\ZipUrls;
use Ttskch\JpPostalCodeApi\FileSystem\BaseDirectoryInterface;
use Ttskch\JpPostalCodeApi\Suggest\SuggestIndexBuilderInterface;

#[AsCommand(
name: 'build',
Expand All @@ -26,6 +27,7 @@ public function __construct(
readonly private CsvParserInterface $kenAllRomeCsvParser,
readonly private CsvParserInterface $jigyosyoCsvParser,
readonly private BaseDirectoryInterface $baseDirectory,
readonly private SuggestIndexBuilderInterface $suggestBuilder,
) {
parent::__construct();
}
Expand All @@ -39,6 +41,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$total = $kenAllCsv->count() + $kenAllRomeCsv->count() + $jigyosyoCsv->count();

$io = new SymfonyStyle($input, $output);

$io->section('Building suggest indices from existing JSON files...');
$this->suggestBuilder->build($this->baseDirectory, $io);
$io->success('Suggest indices built.');

$io->progressStart($total);

$this->baseDirectory->clear();
Expand Down Expand Up @@ -68,6 +75,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$io->progressFinish();
$io->success(sprintf('Finished! %s files are created from %s CSV records.', number_format($this->baseDirectory->countJsonFiles()), number_format($total)));

$io->section('Building suggest indices...');
// 進捗バーは Finder の件数が必要なら2段階に分けてもOK。ここは簡略に。
$this->suggestBuilder->build($this->baseDirectory, $io);
$io->success('Suggest indices built.');

return Command::SUCCESS;
}
}
21 changes: 21 additions & 0 deletions src/FileSystem/BaseDirectory.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,25 @@ private function max(AddressUnit $a, AddressUnit $b): ?AddressUnit

return null;
}
public function getRootPath(): string
{
return rtrim($this->path, '/');
}

public function getJsonRootPath(): string
{
// JSON を直下に置いているならそのまま
return $this->getRootPath();
// もしサブディレクトリに置いているなら、例えば:
// return $this->getRootPath() . '/json';
}

public function ensureDir(string $relative): string
{
$abs = $this->getRootPath() . '/' . ltrim($relative, '/');
if (!is_dir($abs)) {
@mkdir($abs, 0775, true);
}
return $abs;
}
}
9 changes: 9 additions & 0 deletions src/FileSystem/BaseDirectoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,13 @@ public function clear(): void;
public function putJsonFile(ParsedCsvRow $row, bool $en = false): void;

public function countJsonFiles(): int;

/** 出力ルート(例: /path/to/docs)を返す */
public function getRootPath(): string;

/** JSON群のルート(例: getRootPath() と同じ or その配下) */
public function getJsonRootPath(): string;

/** ルート配下にディレクトリを作ってフルパスを返す(存在すればそのまま返す) */
public function ensureDir(string $relative): string;
}
3 changes: 3 additions & 0 deletions src/JpPostalCodeApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Ttskch\JpPostalCodeApi\Csv\CsvParserInterface;
use Ttskch\JpPostalCodeApi\DataSource\CsvProviderInterface;
use Ttskch\JpPostalCodeApi\FileSystem\BaseDirectoryInterface;
use Ttskch\JpPostalCodeApi\Suggest\SuggestIndexBuilderInterface;

final readonly class JpPostalCodeApi
{
Expand All @@ -20,6 +21,7 @@ public function __construct(
private CsvParserInterface $kenAllRomeCsvParser,
private CsvParserInterface $jigyosyoCsvParser,
private BaseDirectoryInterface $baseDirectory,
private SuggestIndexBuilderInterface $suggestBuilder,
?Application $console = null,
) {
$this->console = $console ?? new Application();
Expand All @@ -34,6 +36,7 @@ public function run(): void
$this->kenAllRomeCsvParser,
$this->jigyosyoCsvParser,
$this->baseDirectory,
$this->suggestBuilder ,
));
$this->console->run();
}
Expand Down
129 changes: 129 additions & 0 deletions src/Suggest/SuggestIndexBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php
namespace Ttskch\JpPostalCodeApi\Suggest;

use Ttskch\JpPostalCodeApi\FileSystem\BaseDirectoryInterface;

final class SuggestIndexBuilder implements SuggestIndexBuilderInterface
{
public function __construct(
private readonly bool $buildZip3 = true,
private readonly bool $buildZip7 = true,
private readonly bool $buildLocalities = true,
private readonly bool $buildPrefectures = true,
) {}

/** JSON 1件 = 1郵便番号レコードを想定 */
public function build(BaseDirectoryInterface $baseDir, ?\Symfony\Component\Console\Style\SymfonyStyle $io = null): void
{

// ここを BaseDirectory API に置換
$suggestDir = $baseDir->ensureDir('suggest');

// 出力ファイルを開く(必要なものだけ)
$fhZip3 = $this->buildZip3 ? fopen("$suggestDir/zip3.txt", 'w') : null;
$fhZip7 = $this->buildZip7 ? fopen("$suggestDir/zip7.jsonl", 'w') : null;
$fhLoc = $this->buildLocalities ? fopen("$suggestDir/localities.jsonl", 'w') : null;

// 重複防止(適度に抑える)。巨大なら LRU/一時ファイル等に切替可能
$seenZip3 = [];
$seenCity = []; // key: "{$pref}\t{$city}"
$seenPref = [];

// 生成済みJSONのルートを取得(BaseDirectory が知っている前提)
$root = $baseDir->getJsonRootPath(); // ★ 実装に合わせてください

$jsonFiles = glob($root . '/*.json');
if (!$jsonFiles) {
return; // JSONファイルが存在しない場合は処理を終了
}

foreach ($jsonFiles as $filePath) {
$content = file_get_contents($filePath);
if ($content === false) {
continue;
}
$row = json_decode($content, true, 512, JSON_THROW_ON_ERROR);

// APIResourceの構造に合わせてデータを取得
$zip = $row['postalCode'] ?? null;
$addresses = $row['addresses'] ?? [];

foreach ($addresses as $addr) {
$ja = $addr['ja'] ?? [];
$en = $addr['en'] ?? [];

$pref = $ja['prefecture'] ?? null;
$city = $ja['address1'] ?? null;
$town = trim(($ja['address2'] ?? '') . ' ' . ($ja['address3'] ?? '') . ' ' . ($ja['address4'] ?? ''));

// ローマ字
$prefR = $en['prefecture'] ?? null;
$cityR = $en['address1'] ?? null;
$townR = trim(($en['address2'] ?? '') . ' ' . ($en['address3'] ?? '') . ' ' . ($en['address4'] ?? ''));

if (!$zip || !$pref || !$city) {
continue; // 最低限欠けたらスキップ
}

// 1) zip3
if ($fhZip3) {
$zip3 = substr($zip, 0, 3);
if (!isset($seenZip3[$zip3])) {
fwrite($fhZip3, $zip3 . PHP_EOL);
$seenZip3[$zip3] = true;
}
}

// 2) zip7
if ($fhZip7) {
$label = $pref . ' ' . $city . ($town ? ' ' . $town : '');
$romaji = trim(($prefR ?: '') . ' ' . ($cityR ?: '') . ' ' . ($townR ?: ''));
$item = [
'zip' => $zip,
'label' => $label,
'romaji'=> $romaji ?: null,
'pref' => $pref,
'city' => $city,
'town' => $town ?: null,
'kind' => 'addr',
];
fwrite($fhZip7, json_encode($item, JSON_UNESCAPED_UNICODE) . PHP_EOL);
}

// 3) localities(pref+cityをユニーク化)
if ($fhLoc) {
$k = $pref . "\t" . $city;
if (!isset($seenCity[$k])) {
$romaji = trim(($prefR ?: '') . ' ' . ($cityR ?: ''));
$item = [
'label' => $pref . ' ' . $city,
'romaji'=> $romaji ?: null,
'pref' => $pref,
'city' => $city,
'kind' => 'city',
];
fwrite($fhLoc, json_encode($item, JSON_UNESCAPED_UNICODE) . PHP_EOL);
$seenCity[$k] = true;
}
}

// 4) prefectures(あとで1回で書く)
if ($this->buildPrefectures && $pref) {
$seenPref[$pref] = true;
}
}

// プログレスバーの進捗更新は呼び出し元で管理
}

if ($fhZip3) fclose($fhZip3);
if ($fhZip7) fclose($fhZip7);
if ($fhLoc) fclose($fhLoc);

if ($this->buildPrefectures) {
$list = array_keys($seenPref);
sort($list, SORT_STRING);
file_put_contents("$suggestDir/prefectures.json", json_encode($list, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT));
}
}
}
10 changes: 10 additions & 0 deletions src/Suggest/SuggestIndexBuilderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php
namespace Ttskch\JpPostalCodeApi\Suggest;

use Symfony\Component\Console\Style\SymfonyStyle;
use Ttskch\JpPostalCodeApi\FileSystem\BaseDirectoryInterface;

interface SuggestIndexBuilderInterface
{
public function build(BaseDirectoryInterface $baseDir, ?SymfonyStyle $io = null): void;
}
Loading