jp_address_complement 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. checksums.yaml +7 -0
  2. data/.agent/rules/specify-rules.md +29 -0
  3. data/.agent/workflows/speckit.analyze.md +184 -0
  4. data/.agent/workflows/speckit.checklist.md +294 -0
  5. data/.agent/workflows/speckit.clarify.md +181 -0
  6. data/.agent/workflows/speckit.constitution.md +84 -0
  7. data/.agent/workflows/speckit.implement.md +135 -0
  8. data/.agent/workflows/speckit.plan.md +90 -0
  9. data/.agent/workflows/speckit.specify.md +258 -0
  10. data/.agent/workflows/speckit.tasks.md +137 -0
  11. data/.agent/workflows/speckit.taskstoissues.md +30 -0
  12. data/.claude/commands/speckit.analyze.md +184 -0
  13. data/.claude/commands/speckit.checklist.md +294 -0
  14. data/.claude/commands/speckit.clarify.md +181 -0
  15. data/.claude/commands/speckit.constitution.md +84 -0
  16. data/.claude/commands/speckit.implement.md +135 -0
  17. data/.claude/commands/speckit.plan.md +90 -0
  18. data/.claude/commands/speckit.specify.md +258 -0
  19. data/.claude/commands/speckit.tasks.md +137 -0
  20. data/.claude/commands/speckit.taskstoissues.md +30 -0
  21. data/.cursor/commands/speckit.analyze.md +184 -0
  22. data/.cursor/commands/speckit.checklist.md +294 -0
  23. data/.cursor/commands/speckit.clarify.md +181 -0
  24. data/.cursor/commands/speckit.constitution.md +84 -0
  25. data/.cursor/commands/speckit.implement.md +135 -0
  26. data/.cursor/commands/speckit.plan.md +90 -0
  27. data/.cursor/commands/speckit.specify.md +258 -0
  28. data/.cursor/commands/speckit.tasks.md +137 -0
  29. data/.cursor/commands/speckit.taskstoissues.md +30 -0
  30. data/.cursor/rules/specify-rules.mdc +32 -0
  31. data/.rubocop.yml +118 -0
  32. data/.specify/memory/constitution.md +130 -0
  33. data/.specify/scripts/bash/check-prerequisites.sh +166 -0
  34. data/.specify/scripts/bash/common.sh +156 -0
  35. data/.specify/scripts/bash/create-new-feature.sh +297 -0
  36. data/.specify/scripts/bash/setup-plan.sh +61 -0
  37. data/.specify/scripts/bash/update-agent-context.sh +810 -0
  38. data/.specify/templates/agent-file-template.md +28 -0
  39. data/.specify/templates/checklist-template.md +40 -0
  40. data/.specify/templates/constitution-template.md +50 -0
  41. data/.specify/templates/plan-template.md +104 -0
  42. data/.specify/templates/spec-template.md +115 -0
  43. data/.specify/templates/tasks-template.md +251 -0
  44. data/CHANGELOG.md +26 -0
  45. data/LICENSE +9 -0
  46. data/README.md +274 -0
  47. data/Rakefile +19 -0
  48. data/Steepfile +9 -0
  49. data/examples/rails/jp_address_complement_demo/.gitignore +15 -0
  50. data/examples/rails/jp_address_complement_demo/.ruby-version +1 -0
  51. data/examples/rails/jp_address_complement_demo/Gemfile +22 -0
  52. data/examples/rails/jp_address_complement_demo/Gemfile.lock +252 -0
  53. data/examples/rails/jp_address_complement_demo/README.md +57 -0
  54. data/examples/rails/jp_address_complement_demo/Rakefile +6 -0
  55. data/examples/rails/jp_address_complement_demo/app/assets/images/.keep +0 -0
  56. data/examples/rails/jp_address_complement_demo/app/assets/stylesheets/application.css +1 -0
  57. data/examples/rails/jp_address_complement_demo/app/controllers/addresses_controller.rb +59 -0
  58. data/examples/rails/jp_address_complement_demo/app/controllers/application_controller.rb +4 -0
  59. data/examples/rails/jp_address_complement_demo/app/controllers/concerns/.keep +0 -0
  60. data/examples/rails/jp_address_complement_demo/app/helpers/application_helper.rb +2 -0
  61. data/examples/rails/jp_address_complement_demo/app/models/application_record.rb +3 -0
  62. data/examples/rails/jp_address_complement_demo/app/models/concerns/.keep +0 -0
  63. data/examples/rails/jp_address_complement_demo/app/views/addresses/index.html.erb +22 -0
  64. data/examples/rails/jp_address_complement_demo/app/views/addresses/prefecture.html.erb +20 -0
  65. data/examples/rails/jp_address_complement_demo/app/views/addresses/prefix.html.erb +25 -0
  66. data/examples/rails/jp_address_complement_demo/app/views/addresses/reverse.html.erb +30 -0
  67. data/examples/rails/jp_address_complement_demo/app/views/addresses/validate.html.erb +23 -0
  68. data/examples/rails/jp_address_complement_demo/app/views/layouts/application.html.erb +40 -0
  69. data/examples/rails/jp_address_complement_demo/app/views/pwa/manifest.json.erb +22 -0
  70. data/examples/rails/jp_address_complement_demo/app/views/pwa/service-worker.js +26 -0
  71. data/examples/rails/jp_address_complement_demo/bin/ci +6 -0
  72. data/examples/rails/jp_address_complement_demo/bin/dev +2 -0
  73. data/examples/rails/jp_address_complement_demo/bin/rails +4 -0
  74. data/examples/rails/jp_address_complement_demo/bin/rake +4 -0
  75. data/examples/rails/jp_address_complement_demo/bin/setup +35 -0
  76. data/examples/rails/jp_address_complement_demo/config/application.rb +42 -0
  77. data/examples/rails/jp_address_complement_demo/config/boot.rb +3 -0
  78. data/examples/rails/jp_address_complement_demo/config/ci.rb +15 -0
  79. data/examples/rails/jp_address_complement_demo/config/credentials.yml.enc +1 -0
  80. data/examples/rails/jp_address_complement_demo/config/database.yml +31 -0
  81. data/examples/rails/jp_address_complement_demo/config/environment.rb +5 -0
  82. data/examples/rails/jp_address_complement_demo/config/environments/development.rb +54 -0
  83. data/examples/rails/jp_address_complement_demo/config/environments/production.rb +67 -0
  84. data/examples/rails/jp_address_complement_demo/config/environments/test.rb +42 -0
  85. data/examples/rails/jp_address_complement_demo/config/initializers/content_security_policy.rb +29 -0
  86. data/examples/rails/jp_address_complement_demo/config/initializers/filter_parameter_logging.rb +8 -0
  87. data/examples/rails/jp_address_complement_demo/config/initializers/inflections.rb +16 -0
  88. data/examples/rails/jp_address_complement_demo/config/locales/en.yml +31 -0
  89. data/examples/rails/jp_address_complement_demo/config/puma.rb +39 -0
  90. data/examples/rails/jp_address_complement_demo/config/routes.rb +19 -0
  91. data/examples/rails/jp_address_complement_demo/config.ru +6 -0
  92. data/examples/rails/jp_address_complement_demo/db/migrate/20260228083709_create_jp_address_complement_postal_codes.rb +44 -0
  93. data/examples/rails/jp_address_complement_demo/db/schema.rb +33 -0
  94. data/examples/rails/jp_address_complement_demo/db/seeds.rb +24 -0
  95. data/examples/rails/jp_address_complement_demo/lib/tasks/.keep +0 -0
  96. data/examples/rails/jp_address_complement_demo/log/.keep +0 -0
  97. data/examples/rails/jp_address_complement_demo/public/400.html +135 -0
  98. data/examples/rails/jp_address_complement_demo/public/404.html +135 -0
  99. data/examples/rails/jp_address_complement_demo/public/406-unsupported-browser.html +135 -0
  100. data/examples/rails/jp_address_complement_demo/public/422.html +135 -0
  101. data/examples/rails/jp_address_complement_demo/public/500.html +135 -0
  102. data/examples/rails/jp_address_complement_demo/public/icon.png +0 -0
  103. data/examples/rails/jp_address_complement_demo/public/icon.svg +3 -0
  104. data/examples/rails/jp_address_complement_demo/public/robots.txt +1 -0
  105. data/examples/rails/jp_address_complement_demo/script/.keep +0 -0
  106. data/examples/rails/jp_address_complement_demo/storage/.keep +0 -0
  107. data/examples/rails/jp_address_complement_demo/vendor/.keep +0 -0
  108. data/lib/generators/jp_address_complement/install_generator.rb +34 -0
  109. data/lib/generators/jp_address_complement/templates/create_jp_address_complement_postal_codes.rb.erb +45 -0
  110. data/lib/jp_address_complement/address_record.rb +36 -0
  111. data/lib/jp_address_complement/configuration.rb +21 -0
  112. data/lib/jp_address_complement/importers/csv_importer.rb +148 -0
  113. data/lib/jp_address_complement/ken_all_downloader.rb +122 -0
  114. data/lib/jp_address_complement/models/postal_code.rb +21 -0
  115. data/lib/jp_address_complement/normalizer.rb +77 -0
  116. data/lib/jp_address_complement/prefecture.rb +105 -0
  117. data/lib/jp_address_complement/railtie.rb +27 -0
  118. data/lib/jp_address_complement/repositories/active_record_postal_code_repository.rb +78 -0
  119. data/lib/jp_address_complement/repositories/csv_postal_code_repository.rb +200 -0
  120. data/lib/jp_address_complement/repositories/postal_code_repository.rb +36 -0
  121. data/lib/jp_address_complement/searcher.rb +85 -0
  122. data/lib/jp_address_complement/validators/address_validator.rb +41 -0
  123. data/lib/jp_address_complement/version.rb +6 -0
  124. data/lib/jp_address_complement.rb +129 -0
  125. data/lib/tasks/jp_address_complement.rake +32 -0
  126. data/rbs_collection.lock.yaml +380 -0
  127. data/rbs_collection.yaml +19 -0
  128. data/sig/generated/generators/jp_address_complement/install_generator.rbs +18 -0
  129. data/sig/generated/jp_address_complement/configuration.rbs +18 -0
  130. data/sig/generated/jp_address_complement/importers/csv_importer.rbs +78 -0
  131. data/sig/generated/jp_address_complement/ken_all_downloader.rbs +49 -0
  132. data/sig/generated/jp_address_complement/normalizer.rbs +37 -0
  133. data/sig/generated/jp_address_complement/prefecture.rbs +27 -0
  134. data/sig/generated/jp_address_complement/railtie.rbs +8 -0
  135. data/sig/generated/jp_address_complement/repositories/active_record_postal_code_repository.rbs +38 -0
  136. data/sig/generated/jp_address_complement/repositories/csv_postal_code_repository.rbs +100 -0
  137. data/sig/generated/jp_address_complement/repositories/postal_code_repository.rbs +29 -0
  138. data/sig/generated/jp_address_complement/searcher.rbs +43 -0
  139. data/sig/generated/jp_address_complement/validators/address_validator.rbs +24 -0
  140. data/sig/generated/jp_address_complement/version.rbs +5 -0
  141. data/sig/generated/jp_address_complement.rbs +84 -0
  142. data/sig/manual/address_record.rbs +40 -0
  143. data/sig/manual/gem_rubyzip.rbs +17 -0
  144. data/sig/manual/postal_code.rbs +9 -0
  145. data/sig/manual/stdlib_csv_invalid_encoding_error.rbs +5 -0
  146. data/sig/manual/stdlib_net_http.rbs +33 -0
  147. data/sig/manual/stdlib_openuri.rbs +9 -0
  148. data/sig/manual/stdlib_tmpdir.rbs +4 -0
  149. data/specs/001-jp-address-complement-gem/checklists/requirements.md +36 -0
  150. data/specs/001-jp-address-complement-gem/contracts/public-api.md +209 -0
  151. data/specs/001-jp-address-complement-gem/data-model.md +207 -0
  152. data/specs/001-jp-address-complement-gem/plan.md +124 -0
  153. data/specs/001-jp-address-complement-gem/quickstart.md +151 -0
  154. data/specs/001-jp-address-complement-gem/research.md +139 -0
  155. data/specs/001-jp-address-complement-gem/spec.md +153 -0
  156. data/specs/001-jp-address-complement-gem/tasks.md +279 -0
  157. data/specs/002-rbs-type-annotations/checklists/requirements.md +37 -0
  158. data/specs/002-rbs-type-annotations/contracts/rbs-public-api.md +116 -0
  159. data/specs/002-rbs-type-annotations/data-model.md +119 -0
  160. data/specs/002-rbs-type-annotations/plan.md +116 -0
  161. data/specs/002-rbs-type-annotations/quickstart.md +105 -0
  162. data/specs/002-rbs-type-annotations/research.md +173 -0
  163. data/specs/002-rbs-type-annotations/spec.md +125 -0
  164. data/specs/002-rbs-type-annotations/tasks.md +189 -0
  165. data/specs/003-csv-remove-obsolete/checklists/requirements.md +34 -0
  166. data/specs/003-csv-remove-obsolete/contracts/csv-import.md +41 -0
  167. data/specs/003-csv-remove-obsolete/data-model.md +47 -0
  168. data/specs/003-csv-remove-obsolete/plan.md +73 -0
  169. data/specs/003-csv-remove-obsolete/quickstart.md +40 -0
  170. data/specs/003-csv-remove-obsolete/research.md +71 -0
  171. data/specs/003-csv-remove-obsolete/spec.md +85 -0
  172. data/specs/003-csv-remove-obsolete/tasks.md +167 -0
  173. data/specs/004-prefecture-code-reverse-lookup/checklists/requirements.md +34 -0
  174. data/specs/004-prefecture-code-reverse-lookup/contracts/public-api-prefecture-and-reverse.md +122 -0
  175. data/specs/004-prefecture-code-reverse-lookup/data-model.md +81 -0
  176. data/specs/004-prefecture-code-reverse-lookup/plan.md +92 -0
  177. data/specs/004-prefecture-code-reverse-lookup/quickstart.md +91 -0
  178. data/specs/004-prefecture-code-reverse-lookup/research.md +62 -0
  179. data/specs/004-prefecture-code-reverse-lookup/spec.md +120 -0
  180. data/specs/004-prefecture-code-reverse-lookup/tasks.md +190 -0
  181. metadata +451 -0
@@ -0,0 +1,151 @@
1
+ # クイックスタート: jp_address_complement
2
+
3
+ **バージョン**: 0.1.0 (予定)
4
+ **対応 Ruby**: 3.0 以上
5
+ **対応 Rails**: 7.0 以上
6
+
7
+ ---
8
+
9
+ ## インストール
10
+
11
+ ```ruby
12
+ # Gemfile
13
+ gem 'jp_address_complement'
14
+ ```
15
+
16
+ ```bash
17
+ bundle install
18
+ ```
19
+
20
+ ---
21
+
22
+ ## セットアップ(Rails)
23
+
24
+ ### 1. マイグレーションを生成する
25
+
26
+ ```bash
27
+ rails g jp_address_complement:install
28
+ ```
29
+
30
+ `db/migrate/YYYYMMDDHHMMSS_create_jp_address_complement_postal_codes.rb` が生成されます。
31
+
32
+ ### 2. マイグレーションを実行する
33
+
34
+ ```bash
35
+ rails db:migrate
36
+ ```
37
+
38
+ ### 3. 郵便番号データをインポートする
39
+
40
+ 日本郵便のサイトから KEN_ALL.CSV をダウンロードします。
41
+
42
+ - URL: https://www.post.japanpost.jp/zipcode/dl/oogaki/zip/ken_all.zip
43
+
44
+ ```bash
45
+ # ZIP を展開して CSV を取得後(例: ~/Downloads/KEN_ALL.CSV)
46
+ bundle exec rake jp_address_complement:import CSV=/path/to/KEN_ALL.CSV
47
+ ```
48
+
49
+ 文字コード(Shift_JIS → UTF-8)変換は自動で行われます。
50
+ ダウンロードしたそのままの CSV を指定してください。
51
+
52
+ ---
53
+
54
+ ## 基本的な使い方
55
+
56
+ ### 郵便番号から住所を取得する
57
+
58
+ ```ruby
59
+ # 7桁(ハイフンなし)
60
+ results = JpAddressComplement.search_by_postal_code("1000001")
61
+ # => [#<data JpAddressComplement::AddressRecord postal_code="1000001", pref="東京都", city="千代田区", town="千代田", ...>]
62
+
63
+ # ハイフンあり・〒記号あり も自動正規化
64
+ results = JpAddressComplement.search_by_postal_code("100-0001")
65
+ results = JpAddressComplement.search_by_postal_code("〒100-0001")
66
+
67
+ # 存在しない場合は空配列
68
+ results = JpAddressComplement.search_by_postal_code("0000000")
69
+ # => []
70
+ ```
71
+
72
+ ### 先頭4桁以上で候補を取得する
73
+
74
+ ```ruby
75
+ # 先頭4桁で絞り込み
76
+ candidates = JpAddressComplement.search_by_postal_code_prefix("1000")
77
+ # => [#<data ... postal_code="1000001">, #<data ... postal_code="1000002">, ...]
78
+
79
+ # 先頭3桁以下は空返却(過大結果防止)
80
+ candidates = JpAddressComplement.search_by_postal_code_prefix("100")
81
+ # => []
82
+ ```
83
+
84
+ ### 郵便番号と住所の整合性を検証する
85
+
86
+ ```ruby
87
+ # 都道府県名 + 市区町村名 + 町域名が入力住所に部分一致すれば true
88
+ JpAddressComplement.valid_combination?("1000001", "東京都千代田区千代田1-1")
89
+ # => true(番地が付加されていても true)
90
+
91
+ JpAddressComplement.valid_combination?("1000001", "大阪府大阪市北区")
92
+ # => false
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Rails モデルでバリデーションを使う
98
+
99
+ ```ruby
100
+ class Order < ApplicationRecord
101
+ validates :postal_code, presence: true
102
+ validates :postal_code, jp_address_complement: { address_field: :address }
103
+ end
104
+ ```
105
+
106
+ ```ruby
107
+ order = Order.new(postal_code: "1000001", address: "東京都千代田区千代田1-1 ○○ビル")
108
+ order.valid? # => true
109
+
110
+ order2 = Order.new(postal_code: "1000001", address: "大阪府大阪市北区")
111
+ order2.valid? # => false
112
+ order2.errors[:postal_code] # => ["と住所が一致しません"]
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Rails 以外の環境での使用(Rack / Sinatra)
118
+
119
+ ```ruby
120
+ require 'jp_address_complement'
121
+ require 'my_custom_repository' # 利用者が用意するリポジトリ実装
122
+
123
+ # カスタムリポジトリを注入
124
+ JpAddressComplement.configure do |config|
125
+ config.repository = MyCustomPostalCodeRepository.new
126
+ end
127
+
128
+ # 以降は通常どおり使用可能
129
+ results = JpAddressComplement.search_by_postal_code("1000001")
130
+ ```
131
+
132
+ ---
133
+
134
+ ## データ更新
135
+
136
+ ```bash
137
+ # 最新の KEN_ALL.CSV をダウンロードして再インポート
138
+ bundle exec rake jp_address_complement:import CSV=/path/to/new/KEN_ALL.CSV
139
+ ```
140
+
141
+ 既存データは upsert(追加・更新)されます。更新タイミングは利用者が任意に決定してください。
142
+
143
+ ---
144
+
145
+ ## トラブルシューティング
146
+
147
+ | 問題 | 対処 |
148
+ |------|------|
149
+ | `jp_address_complement_postal_codes` テーブルが存在しない | `rails g jp_address_complement:install && rails db:migrate` を実行 |
150
+ | 検索結果が空になる | `rake jp_address_complement:import` でデータをインポートしているか確認 |
151
+ | 文字化けが起きる | KEN_ALL.CSV はそのまま渡してください(変換は自動) |
@@ -0,0 +1,139 @@
1
+ # Research: 日本住所補完 Gem — Phase 0 調査結果
2
+
3
+ **Branch**: `001-jp-address-complement-gem`
4
+ **Date**: 2026-02-22
5
+ **Status**: Complete
6
+
7
+ ---
8
+
9
+ ## 1. Repository パターン(Rails 非依存コア)
10
+
11
+ ### Decision
12
+ コア検索ロジックは `PostalCodeRepository` インターフェースを介してデータアクセスする。
13
+ Rails 環境向けに `ActiveRecordPostalCodeRepository` を同梱し、他環境では利用者がアダプターを注入できる設計とする。
14
+
15
+ ### Rationale
16
+ - Repository が返す値は Plain Ruby の `AddressRecord` 構造体(Struct/Data)とする。ActiveRecord インスタンスを返さないことで、コアロジックへの AR 依存を完全に排除できる。
17
+ - テスト時にはメモリ実装(`FakePostalCodeRepository`)を注入することで、DBへの依存なくコアロジックを単体テストできる(TDD 親和性 ◎)。
18
+
19
+ ### Alternatives Considered
20
+ - **ActiveRecord 直接依存**: 最もシンプルだが Rails 以外での利用が不可能になり FR-015 に違反する。
21
+ - **rom-rb (Ruby Object Mapper)**: データマッパーパターンで AR 依存を排除できるが、利用者側にも rom-rb の学習コストが生じ、シンプルさ原則(VI)に反する。
22
+ - **dry-rb/dry-container でのDI**: 本格的な DI コンテナで柔軟性が高いが YAGNI 違反。シンプルなコンストラクタ注入で十分。
23
+
24
+ ### Interface Design
25
+ ```ruby
26
+ module JpAddressComplement
27
+ module Repositories
28
+ # インターフェース(Duck typing、RBS で型定義も提供)
29
+ class PostalCodeRepository
30
+ # @param code [String] 7桁郵便番号
31
+ # @return [Array<AddressRecord>]
32
+ def find_by_code(code) = raise NotImplementedError
33
+
34
+ # @param prefix [String] 4桁以上の前方一致プレフィックス
35
+ # @return [Array<AddressRecord>]
36
+ def find_by_prefix(prefix) = raise NotImplementedError
37
+ end
38
+ end
39
+ end
40
+ ```
41
+
42
+ ---
43
+
44
+ ## 2. Railtie / Generator 設計
45
+
46
+ ### Decision
47
+ - `JpAddressComplement::Railtie` を通じて Rails に統合する。
48
+ - `rails g jp_address_complement:install` でマイグレーションファイルを `db/migrate/` に生成する(Devise パターン)。
49
+ - Railtie の initializer でデフォルトリポジトリ(`ActiveRecordPostalCodeRepository`)を DI コンテナへ登録する。
50
+
51
+ ### Rationale
52
+ - Railtie は軽量で、モデル・ビューを持たない Gem では Engine より Railtie の方が適切(Constitution I 準拠)。
53
+ - Generator によるマイグレーション生成は Rails コミュニティ標準のパターン(devise, kaminari 等と同様)。
54
+ - Rails 以外の環境では Railtie がロードされないため、コア機能に影響しない。
55
+
56
+ ### Alternatives Considered
57
+ - **Rails::Engine**: 不要なルーティング/ビュー機能が含まれ、オーバースペック。
58
+ - **自動テーブル作成**: 利用者の制御が失われる。standard_procedures に反する。
59
+
60
+ ---
61
+
62
+ ## 3. CSV インポート — 文字コード変換
63
+
64
+ ### Decision
65
+ `CSV.foreach(path, encoding: 'Shift_JIS:UTF-8', ...)` で Shift_JIS→UTF-8 変換をストリーミング処理する。Ruby 標準ライブラリのみで実現可能。
66
+
67
+ ### Rationale
68
+ - Ruby の `CSV.foreach` は `encoding: 'src:dst'` 形式でトランスコードをストリーム処理できる。
69
+ - メモリ効率が高く(全件をメモリに載せない)、12万件程度では問題なし。
70
+ - 外部ライブラリ不要でシンプルさ原則(VI)に合致。
71
+
72
+ ---
73
+
74
+ ## 4. バッチ upsert — パフォーマンス設計
75
+
76
+ ### Decision
77
+ `ActiveRecord::Base.connection.upsert_all`(Rails 6+ 標準)を 1,000 件/バッチで分割実行する。
78
+
79
+ ### Benchmark Evidence
80
+ | 件数 | 手法 | 所要時間 |
81
+ |------|------|---------|
82
+ | 100,000 | `upsert_all` | 約 3〜4 秒 |
83
+ | 100,000 | `activerecord-import` | 約 4.8 秒 |
84
+ | 1,000,000 | `upsert_all` | 約 48.7 秒 |
85
+
86
+ - 12万件 ✕ upsert_all(1,000件/バッチ)= 推定 **3〜5 秒**。SC-004(60秒以内)を十分に満足。
87
+ - Rails 7 の `upsert_all` は `update_only` / `record_timestamps` オプションで制御が容易。
88
+
89
+ ### Alternatives Considered
90
+ - **activerecord-import gem**: バリデーション付きインポートが必要な場合に検討。今回は Rake タスクが事前バリデーション済み CSV を前提とするため不要。
91
+ - **PostgreSQL COPY コマンド**: 100万件超でメリットが出るが、DB 非依存が必要な本 Gem では採用しない。
92
+
93
+ ### Idempotency Strategy
94
+ - `upsert_all` の unique key: `postal_code` カラム
95
+ - 同一郵便番号が複数レコード(大口事業所等)存在するため、`(postal_code, city, town)` の複合キーを一意制約の対象とする。
96
+ - 削除された郵便番号の検出は初期版では対応しない(Deferred)。
97
+
98
+ ---
99
+
100
+ ## 5. 文字列照合(`valid_combination?`)
101
+
102
+ ### Decision
103
+ `String#include?` で部分一致検索する。照合対象は「都道府県名 + 市区町村名 + 町域名」の連結文字列。
104
+
105
+ ```ruby
106
+ address_string.include?(record.pref + record.city + record.town)
107
+ ```
108
+
109
+ ### Rationale
110
+ - DB クエリではなくアプリ層で判定するため、高速かつシンプル。
111
+ - 同一郵便番号に複数レコードがある場合は `any?` で評価。
112
+ - 全角・半角の正規化は照合前に実施(`Unicode::NFC` または `String#unicode_normalize`)。
113
+
114
+ ---
115
+
116
+ ## 6. DB スキーマ設計
117
+
118
+ ### インデックス戦略
119
+ - `postal_codes` テーブル
120
+ - PK: `id`(`bigint`)
121
+ - `postal_code VARCHAR(7)`: インデックス(完全一致検索 O(log n))
122
+ - `postal_code_prefix VARCHAR(3)`: 前方一致高速化のための派生カラム(先頭3桁)をインデックス化、またはプレフィックス LIKE インデックス
123
+ - `pref_code VARCHAR(2)`, `pref VARCHAR(10)`, `city VARCHAR(50)`, `town VARCHAR(100)`, `kana_pref`, `kana_city`, `kana_town`
124
+
125
+ ### LIKE インデックス vs. 派生カラム
126
+ - PostgreSQL: `LIKE 'prefix%'` は B-tree インデックスで最適化可能(`text_pattern_ops`)
127
+ - MySQL/MariaDB: `LIKE 'prefix%'` は先頭一致なら B-tree インデックスを使用
128
+ - SQLite: 同様に先頭一致 LIKE は B-tree インデックスが有効
129
+ - **結論**: 追加カラム不要。クエリを `WHERE postal_code LIKE ?` (`'1000%'`)で発行し、`postal_code` への B-tree インデックスで SC-003(50ms)を達成可能。
130
+
131
+ ---
132
+
133
+ ## 7. 未解決事項(Deferred)
134
+
135
+ | 項目 | 理由 |
136
+ |------|------|
137
+ | 削除済み郵便番号の検出 | 初期版スコープ外。差分インポートの複雑化を避ける(YAGNI) |
138
+ | RBS 型定義の提供 | 実装後に追加可能。初期版では不要 |
139
+ | 差分専用インポート(差分 CSV 活用) | 日本郵便の差分 CSV 形式が変動するリスクがあるため延期 |
@@ -0,0 +1,153 @@
1
+ # Feature Specification: 日本住所補完 Gem
2
+
3
+ **Feature Branch**: `001-jp-address-complement-gem`
4
+ **Created**: 2026-02-22
5
+ **Status**: Draft
6
+ **Input**: User description: "日本国内の住所を郵便番号や住所の一部から補完できるRails 7.x以上向けのgemを作ります"
7
+
8
+ ## User Scenarios & Testing *(mandatory)*
9
+
10
+ ### User Story 1 - 郵便番号から住所を取得する (Priority: P1)
11
+
12
+ Rails アプリ開発者が郵便番号(7桁)を入力すると、対応する都道府県・市区町村・町域名を取得できる。
13
+ フォームの住所欄を自動補完したり、入力済み郵便番号と住所の整合性を確認するために使用する。
14
+
15
+ **Why this priority**: 住所補完の最も基本的な利用パターンであり、この機能だけで即時に利用価値がある。
16
+
17
+ **Independent Test**: `JpAddressComplement.search_by_postal_code("1000001")` を呼び出し、千代田区千代田の住所データが返ることを確認することで独立してテスト可能。
18
+
19
+ **Acceptance Scenarios**:
20
+
21
+ 1. **Given** 有効な7桁郵便番号(例: "1000001")を与えた時、**When** 住所検索を実行すると、**Then** 都道府県・市区町村・町域名を含む住所オブジェクトが返る。
22
+ 2. **Given** ハイフンあり郵便番号(例: "100-0001")を与えた時、**When** 住所検索を実行すると、**Then** ハイフンを除去して検索し、正しい住所が返る。
23
+ 3. **Given** 存在しない郵便番号を与えた時、**When** 住所検索を実行すると、**Then** `[]`(空配列)が返る。
24
+ 4. **Given** 郵便番号のフォーマットが不正(桁数不足・数字以外含む)の時、**When** 住所検索を実行すると、**Then** エラーを発生させずに空の結果が返る。
25
+
26
+ ---
27
+
28
+ ### User Story 2 - 郵便番号の先頭数字から候補住所を取得する (Priority: P2)
29
+
30
+ Rails アプリ開発者が郵便番号の先頭4桁以上を入力すると、該当する住所候補の一覧を取得できる。
31
+ ユーザーが郵便番号を入力中にリアルタイムでサジェストを表示するために使用する。
32
+
33
+ **Why this priority**: インクリメンタルサーチによる補完UXを実現するために必要であり、P1の検索機能をベースに拡張できる。
34
+
35
+ **Independent Test**: `JpAddressComplement.search_by_postal_code_prefix("1000")` を呼び出し、"1000" で始まる複数の住所候補が返ることを確認することで独立してテスト可能。
36
+
37
+ **Acceptance Scenarios**:
38
+
39
+ 1. **Given** 先頭4桁以上の郵便番号(例: "1000")を与えた時、**When** 候補検索を実行すると、**Then** その番号から始まる複数の住所候補リストが返る。
40
+ 2. **Given** 先頭4桁未満の文字列(例: "10")を与えた時、**When** 候補検索を実行すると、**Then** 空の結果が返る(候補が多すぎるため最低桁数制限)。
41
+ 3. **Given** 7桁の完全な郵便番号を与えた時、**When** 候補検索を実行すると、**Then** 対応する住所のみ(1件または0件)が返る。
42
+
43
+ ---
44
+
45
+ ### User Story 3 - 郵便番号と住所の整合性を検証する (Priority: P3)
46
+
47
+ Rails アプリ開発者が郵便番号と住所文字列のペアを渡すと、それらが整合しているかどうかを検証できる。
48
+ 入力フォームのバリデーションやデータクレンジングに使用する。
49
+
50
+ **Why this priority**: 単体の住所補完よりも活用ケースは限定されるが、データ品質担保の観点で重要な機能。
51
+
52
+ **Independent Test**: `JpAddressComplement.valid_combination?("1000001", "東京都千代田区千代田")` を呼び出し、`true` が返ることを確認することで独立してテスト可能。
53
+
54
+ **Acceptance Scenarios**:
55
+
56
+ 1. **Given** 正しい郵便番号と、対応レコードの「都道府県名」「市区町村名」「町域名」を含む住所文字列を与えた時、**When** 整合性検証を実行すると、**Then** `true` が返る(番地・建物名等が付加されていても `true`)。
57
+ 2. **Given** 郵便番号に対応しない住所を与えた時、**When** 整合性検証を実行すると、**Then** `false` が返る。
58
+ 3. **Given** 存在しない郵便番号を与えた時、**When** 整合性検証を実行すると、**Then** `false` が返る。
59
+
60
+ ---
61
+
62
+ ### User Story 4 - Rails モデルでバリデーションを使用する (Priority: P4)
63
+
64
+ Rails アプリ開発者が ActiveRecord モデルに1行のバリデーション宣言を追加するだけで、郵便番号と住所フィールドの整合性チェックを自動化できる。
65
+
66
+ **Why this priority**: Rails エコシステムへの統合を提供し、日常的な開発ワークフローに組み込みやすくする。
67
+
68
+ **Independent Test**: `validates :postal_code, jp_address_complement: { address_field: :address }` を記述したモデルをテストし、不整合データのバリデーション失敗を確認することで独立してテスト可能。
69
+
70
+ **Acceptance Scenarios**:
71
+
72
+ 1. **Given** Rails モデルに住所バリデーターを設定し、整合する郵便番号と住所を持つレコードを与えた時、**When** バリデーションを実行すると、**Then** バリデーションが通過する。
73
+ 2. **Given** Rails モデルに住所バリデーターを設定し、不整合な郵便番号と住所を持つレコードを与えた時、**When** バリデーションを実行すると、**Then** バリデーションが失敗し、エラーメッセージが付与される。
74
+ 3. **Given** Rails モデルに住所バリデーターを設定し、郵便番号フィールドが空のレコードを与えた時、**When** バリデーションを実行すると、**Then** 住所補完バリデーターはスキップされる(他のバリデーターに委ねる)。
75
+
76
+ ---
77
+
78
+ ### Edge Cases
79
+
80
+ - 郵便番号が全角数字で入力された場合はどうなるか?(半角変換して処理する)
81
+ - 同一郵便番号に複数の町域が対応する場合(大口事業所専用番号など)は全件返す。
82
+ - データが存在しない過去・廃止された郵便番号を与えた場合は空を返す。
83
+ - 住所文字列に読みがなや英数字が混在する場合でも、漢字表記部分で照合する。
84
+ - データファイルが破損・欠損している場合は明確なエラーを発生させる。
85
+
86
+ ## Requirements *(mandatory)*
87
+
88
+ ### Functional Requirements
89
+
90
+ - **FR-001**: Gem は郵便番号(7桁、数字のみ)から都道府県・市区町村・町域名を含む住所データを返せなければならない。
91
+ - **FR-002**: Gem はハイフン("〒100-0001" 形式)を含む郵便番号を自動で正規化(除去)して処理しなければならない。
92
+ - **FR-003**: Gem は郵便番号の先頭4桁以上を引数として受け取り、それに該当する住所候補の一覧を返せなければならない。
93
+ - **FR-004**: 先頭4桁未満の入力に対する候補検索は、空の結果を返さなければならない(過大結果の防止)。
94
+ - **FR-005**: Gem は郵便番号と住所文字列を受け取り、両者の整合性を boolean で返せなければならない。整合性の判定は、郵便番号に対応するレコードの「都道府県名」「市区町村名」「町域名」を連結した文字列が、入力住所文字列に『部分一致』するかどうかで行う。番地・建物名等が付加されていても一致とみなす。
95
+ - 同一郵便番号に複数レコードがある場合は、いずれか一件に一致すれば `true` を返す。
96
+ - **FR-006**: Gem は Rails 7.x 以上で利用できる ActiveModel バリデーターを提供しなければならない。
97
+ - **FR-007**: バリデーターは郵便番号フィールドと住所フィールドを関連付けて整合性を自動チェックしなければならない。
98
+ - **FR-008**: 住所データは日本郵便が公開する郵便番号データ(KEN_ALL.CSV)を元に、利用アプリケーションのDBに保持しなければならない。Gem 自体はデータファイルをバンドルしない。
99
+ - **FR-009**: 住所データの更新は、Gem が提供する Rake タスク(例: `jp_address_complement:import`)の実行により完結しなければならない。更新タイミングの判断と実行は利用者に委ねる。
100
+ - **FR-013**: Rake タスクは KEN_ALL.CSV のパスを引数として受け取り、Shift_JIS から UTF-8 への文字コード変換を自動で行いながら DB に全件インポート(既存データは upsert)する機能を提供しなければならない。利用者は日本郵便からダウンロードした CSV をそのまま指定するだけでよい。
101
+ - **FR-014**: Gem は Rails Generator(`rails g jp_address_complement:install`)を提供し、住所テーブル用マイグレーションファイルを利用者アプリの `db/migrate/` 配下に生成しなければならない。テーブルの適用は利用者が `rails db:migrate` を実行することで行う。
102
+ - **FR-010**: Gem のコア検索ロジックは ActiveRecord に直接依存せず、Repository インターフェースを介してデータアクセスを行わなければならない。Rails 環境では ActiveRecord 実装のリポジトリを Gem が同梱し、Rack/Sinatra 等の環境では利用者が任意のアダプターを注入できる設計とする。
103
+ - **FR-015**: Gem は Rails 以外の環境でも、Repository アダプターを注入することでコア検索機能(郵便番号検索・候補検索・整合性検証)を利用できなければならない。
104
+ - **FR-011**: 検索結果の住所データは、少なくとも郵便番号・都道府県名・市区町村名・町域名の4フィールドを含まなければならない。
105
+ - **FR-012**: 不正な入力(nil、空文字、数字以外)に対してはエラーを発生させず、空の結果を返さなければならない。
106
+
107
+ ### Key Entities
108
+
109
+ - **住所レコード (AddressRecord)**: 郵便番号・都道府県コード・都道府県名・市区町村名・町域名・読みがなを保持するデータ単位。郵便番号をキーに一意または複数存在しうる。DBの1行に対応する。
110
+ - **住所テーブル (`jp_address_complement_postal_codes`)**: 利用アプリケーションのDBに存在する住所レコードの永続化テーブル。KEN_ALL.CSV からインポートされた全レコードを格納し、検索クエリに使用される。`PostalCode` ActiveRecord モデルに対応する。起動時のメモリ読み込みは行わない。
111
+ - **住所バリデーター (AddressValidator)**: ActiveModel::Validator を継承し、モデルの郵便番号フィールドと住所フィールドの整合性を検証する Rails コンポーネント。
112
+ - **インストールジェネレーター (InstallGenerator)**: `rails g jp_address_complement:install` で呼び出される Rails Generator。住所テーブル用マイグレーションファイルを利用者アプリの `db/migrate/` に生成する。
113
+ - **郵便番号リポジトリ (PostalCodeRepository)**: データアクセスを抽象化するインターフェース層。郵便番号完全一致検索・前方一致検索のメソッドを定義する。Gem は Rails 向け ActiveRecord 実装(`ActiveRecordPostalCodeRepository`)を同梱し、他環境では利用者が任意の実装を注入する。
114
+
115
+ ## Success Criteria *(mandatory)*
116
+
117
+ ### Measurable Outcomes
118
+
119
+ - **SC-001**: 郵便番号による住所検索の応答が、DBインポート済みの状態で 10 ミリ秒以内に完了する(99パーセンタイル、適切なインデックスが設定されていること前提)。
120
+ - **SC-002**: 日本郵便の公式データに収録されている全郵便番号(約12万件)について、正しい住所が返る。
121
+ - **SC-003**: 先頭4桁による候補検索が 50 ミリ秒以内に完了する(候補件数に関わらず)。
122
+ - **SC-004**: Gem はアプリケーション起動時にメモリへのデータ読み込みを行わないため、Rails アプリの起動時間に影響を与えない。Rake タスクによる全件インポート(約12万件)は完了まで最大 60 秒を許容する。
123
+ - **SC-005**: コード品質として自動テストカバレッジ 90% 以上を達成する。
124
+ - **SC-006**: 住所バリデーターを Rails モデルに組み込む手順が 3 行以内のコードで完結する。
125
+
126
+ ### Constitution Compliance Criteria
127
+
128
+ > これらの基準は Constitution に基づく必須成功条件です。
129
+
130
+ - **CC-001**: `bundle exec rspec` が全テスト PASS し、SimpleCov カバレッジが **90% 以上**である。
131
+ - **CC-002**: `bundle exec rubocop` が PASS(警告・エラーゼロ、`rubocop:disable` コメントなし)。
132
+ - **CC-003**: すべての実装コードは先にテストを書き、Red-Green-Refactor サイクルを経ている(TDD)。
133
+ - **CC-004**: gem は `JpAddressComplement` 名前空間に閉じており、コア検索ロジックは Repository インターフェースを介してデータアクセスし、ActiveRecord への直接依存を持たない。Rails 環境向けに ActiveRecord 実装を同梱し、他環境ではアダプター注入で動作する。
134
+
135
+ ## Assumptions
136
+
137
+ - 住所データのソースは日本郵便が無償で公開する KEN_ALL.CSV(全国版)とする。
138
+ - 住所データは利用アプリケーションのDBに保持する。Gem はデータファイルをバンドルしない。データの投入・更新タイミングは利用者が任意に決定し、Gem が提供する Rake タスクを実行することで反映する。
139
+ - 住所の文字コードは UTF-8 とする。Rake タスクが KEN_ALL.CSV(Shift_JIS)を読み込む際に自動で UTF-8 へ変換する。利用者は日本郵便から直接ダウンロードした CSV をそのまま Rake タスクへ渡すだけでよい。
140
+ - 全角数字の郵便番号は自動で半角に正規化して処理する(追加コストが小さいため)。
141
+ - 読みがなフィールドは返却データに含めるが、読みがなによる検索機能は本仕様のスコープ外とする。
142
+ - 大口事業所・私書箱向けの専用郵便番号も同一データに含め、同様に検索可能とする。
143
+ - Rails への依存は ActiveModel バリデーター提供部分のみに限定し、コア検索ロジックは Rack/Sinatra でも利用可能とする。
144
+
145
+ ## Clarifications
146
+
147
+ ### Session 2026-02-22
148
+
149
+ - Q: データ保持のライフサイクル方針(起動時メモリロード / DB永続化 / 都度ファイル読み込み)はどれか? → A: データはDBに保持し、更新はRakeタスクのみ提供、更新のトリガーは利用者に委ねる。起動時にメモリへの住所情報読み込みは行わない。
150
+ - Q: DBテーブルのスキーマ管理方法(自動作成 / Generator+db:migrate / 専用DB分離)はどれか? → A: Generator(`rails g jp_address_complement:install`)でマイグレーションファイルを利用者アプリに生成し、`rails db:migrate` で適用するRails標準パターン。
151
+ - Q: Rails非依存コアの実現方針(Repository抽象化 / ActiveRecord gem直接依存 / Rails尢用に割り切り)はどれか? → A: Repository パターンで抽象化。Rails環境はActiveRecord実装を同梱し、Rack/Sinatra等では利用者がアダプターを注入する疎結合設計。
152
+ - Q: 整合性検証(`valid_combination?`)の照合粒度はどれか? → A: 郵便番号対応レコードの都道府県名+市区町村名+町域名の連結含文字列が入力住所に部分一致すれば `true`。番地・建物名を含む文字列でも `true` とみなす。
153
+ - Q: Rakeタスクインポート時の文字コード変換責務はどこか? → A: Rakeタスクが Shift_JIS→UTF-8 変換を自動実行。利用者はダウンロードしたままの CSV をそのまま指定するだけでよい。