psgc-rb 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d1fd4a6936426a2a2efc694827ffd4d5fd0d33e13a8933ff5df64626edfb1ab
4
- data.tar.gz: 552c45ca68904de0614b5d474ba1e055e4b3ef4139b80d6ecad0314de6b97a3c
3
+ metadata.gz: a1bfa699725cdd1d7ab90c3086d4fa00206eba2fa8bf588a305f9a3c114f9e94
4
+ data.tar.gz: e58c17f37ca5d80825539bc410cc53725491a1fb1c27a278bab6cef540f9b1f1
5
5
  SHA512:
6
- metadata.gz: e4e8fd85a964fefa515233771a7551fe3dbd5070ab583aa9d8e45caefd76d084816e5fa46d6f0316c77485ae2da9b3dc5ff77b5594ac982d1d8692e5c74bbd7e
7
- data.tar.gz: 7734822468ff709b6b093162220773ce207619ac5453481c4b58a9abeab4e2a555b75ea03533980811a638f7e99570d9e66d6f02ffe27622ac155da14bab98e8
6
+ metadata.gz: 51ffa2a2928c0e72f75f8bb316c261ac710901c86106583bc40e11b13a5d7205db24ab53fc8a9ceb43ff74793b7ffabd024fec22b231cb9e69fd058bea6ca819
7
+ data.tar.gz: a5e6fdd95d4455cdabe57a3ec6b5394d8be32c7d6d91f005e97d494c0b6e68ef21b02c1d0ea08e26c91c41187ac85c0e3b85d342eb29c861025526212f84d4c4
data/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [0.2.0] - 2026-04-25
6
+
7
+ ### Added
8
+ - Finder methods (`find_region`, `find_province`, `find_city_municipality`, `find_barangay`) for looking up by code or name
9
+ - `hierarchy` method for traversing PSGC relationships (e.g., barangay → city → province → region)
10
+ - `search` method for fuzzy name matching across all geographic levels
11
+ - `valid?` method to check if a PSGC code exists
12
+ - `stats` method to get counts by geographic level
13
+ - CLI tool with `find`, `hierarchy`, `valid`, `stats`, and `export` commands
14
+ - Rails seed generator (`rails generate psgc:seed`)
15
+ - Export methods (`export_csv`, `export_yaml`, `export_geojson`)
16
+ - Support for `:cities` as alias for `:cities_municipalities` level
17
+
18
+ ### Changed
19
+ - Default export level changed from `:barangays` to `:regions` for safer preview
20
+
21
+ ## [0.1.0] - 2026-04-25
22
+
23
+ ### Added
24
+ - Initial release with data loaders for PSGC regions, provinces, cities/municipalities, and barangays
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  A Ruby gem providing up-to-date Philippine geographic data from the PSA (Philippine Statistics Authority). Includes Philippine Standard Geographic Codes (PSGC) for regions, provinces, cities/municipalities, and barangays.
4
4
 
5
+ Requires Ruby >= 3.2.0.
6
+
5
7
  ## Installation
6
8
 
7
9
  ```bash
@@ -14,6 +16,8 @@ Or add to Gemfile:
14
16
  gem "psgc-rb"
15
17
  ```
16
18
 
19
+ Note: If using the Rails generator, Rails >= 6.1 is required. Each target model (Region, Province, City, or Barangay) must have a unique index on its code column for upsert_all to work.
20
+
17
21
  ## Usage
18
22
 
19
23
  ```ruby
@@ -34,8 +38,94 @@ Psgc.cities_municipalities
34
38
  # Get all barangays
35
39
  Psgc.barangays
36
40
  # => [{:code=>"1400101001", :name=>"Bagtayan", :city_municipality_code=>"1400101000"}, ...]
41
+
42
+ # Foreign-key fields (e.g., :region_code on a province, :province_code on a city)
43
+ # store a prefix of the parent's full 10-digit code.
44
+
45
+ # Finder methods
46
+ Psgc.find_region(code: "1300000000")
47
+ # => {:code=>"1300000000", :name=>"National Capital Region (NCR)"}
48
+
49
+ Psgc.find_province(code: "1400100000")
50
+ # => {:code=>"1400100000", :name=>"Abra", :region_code=>"14"}
51
+
52
+ Psgc.find_city_municipality(code: "1400101000")
53
+ # => {:code=>"1400101000", :name=>"Bangued", :province_code=>"1400"}
54
+
55
+ Psgc.find_barangay(code: "1400101001")
56
+ # => {:code=>"1400101001", :name=>"Bagtayan", :city_municipality_code=>"1400101000"}
57
+
58
+ # Find by name (case-insensitive substring match)
59
+ Psgc.find_region(name: "National Capital")
60
+ # => {:code=>"1300000000", :name=>"National Capital Region (NCR)"}
61
+
62
+ # Hierarchy traversal
63
+ Psgc.hierarchy("1400101001")
64
+ # => {:code=>"1400101001", :barangay=>{...}, :city_municipality=>{...}, :province=>{...}, :region=>{...}}
65
+
66
+ # Search across all levels
67
+ Psgc.search("cebu")
68
+ # => {:regions=>[...], :provinces=>[...], :cities_municipalities=>[...], :barangays=>[...]}
69
+
70
+ Psgc.search("san", limit: 5)
71
+ # => {:regions=>[...], :provinces=>[...], :cities_municipalities=>[...], :barangays=>[...]}
72
+ # (each level capped at 5 results)
73
+
74
+ # Validate PSGC code
75
+ Psgc.valid?("1400101001") # => true
76
+ Psgc.valid?("9999999999") # => false
77
+
78
+ # Statistics
79
+ Psgc.stats
80
+ # => {:regions=>Integer, :provinces=>Integer, :cities_municipalities=>Integer, :barangays=>Integer}
81
+
82
+ # Export data
83
+ Psgc.export_csv(level: :regions)
84
+ Psgc.export_csv(level: :provinces)
85
+ Psgc.export_csv(level: :cities_municipalities) # or :cities
86
+ Psgc.export_csv(level: :barangays)
87
+
88
+ Psgc.export_yaml(level: :regions)
89
+ Psgc.export_geojson(level: :regions)
37
90
  ```
38
91
 
92
+ ## CLI
93
+
94
+ ```bash
95
+ # Find by name
96
+ psgc find Manila
97
+ psgc find Cebu --limit 10
98
+
99
+ # Show hierarchy
100
+ psgc hierarchy 1400101001
101
+
102
+ # Validate code
103
+ psgc valid 1400101001
104
+
105
+ # Statistics
106
+ psgc stats
107
+
108
+ # Export data
109
+ psgc export --csv
110
+ psgc export --csv --level=provinces
111
+ psgc export --yaml --level=barangays
112
+ psgc export --geojson --level=cities
113
+ ```
114
+
115
+ ## Rails Generator
116
+
117
+ Generate seed data for your Rails app (requires Rails >= 6.1):
118
+
119
+ ```bash
120
+ rails generate psgc:seed
121
+ rails generate psgc:seed --level=provinces
122
+ rails generate psgc:seed --level=cities
123
+ rails generate psgc:seed --level=barangays
124
+ rails generate psgc:seed --region-model=Region --province-model=Province --city-model=City --barangay-model=Barangay
125
+ ```
126
+
127
+ Valid levels: `all` (default), `regions`, `provinces`, `cities` or `cities_municipalities`, `barangays`.
128
+
39
129
  ## Data Source
40
130
 
41
131
  Data is sourced from the PSA PSGC Publication Datafile, released quarterly.
@@ -51,6 +141,8 @@ To update data:
51
141
 
52
142
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
53
143
 
144
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for data update instructions.
145
+
54
146
  ## License
55
147
 
56
- The gem is available as open source under the terms of the MIT License.
148
+ Released under the [MIT License](./LICENSE).
data/exe/psgc ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
+ require "psgc"
6
+ require "psgc/cli"
7
+
8
+ exit(Psgc::CLI.run(ARGV) ? 0 : 1)
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module Psgc
6
+ module Generators
7
+ class SeedGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ VALID_LEVELS = %w[all regions provinces cities cities_municipalities barangays].freeze
11
+
12
+ class_option :level,
13
+ type: :string,
14
+ default: "all",
15
+ desc: "Geographic level: all, regions, provinces, cities/cities_municipalities, barangays"
16
+
17
+ class_option :region_model,
18
+ type: :string,
19
+ default: "Region",
20
+ desc: "Model name for regions"
21
+
22
+ class_option :province_model,
23
+ type: :string,
24
+ default: "Province",
25
+ desc: "Model name for provinces"
26
+
27
+ class_option :city_model,
28
+ type: :string,
29
+ default: "City",
30
+ desc: "Model name for cities/municipalities"
31
+
32
+ class_option :barangay_model,
33
+ type: :string,
34
+ default: "Barangay",
35
+ desc: "Model name for barangays"
36
+
37
+ def create_seed_file
38
+ validate_level!
39
+ template "seed.rb.erb", "db/seeds.rb"
40
+ end
41
+
42
+ private
43
+
44
+ def validate_level!
45
+ return if VALID_LEVELS.include?(options[:level])
46
+
47
+ raise ArgumentError, "Invalid level: #{options[:level]}. Valid: #{VALID_LEVELS.join(', ')}"
48
+ end
49
+
50
+ def level
51
+ options[:level]
52
+ end
53
+
54
+ def region_model
55
+ options[:region_model]
56
+ end
57
+
58
+ def province_model
59
+ options[:province_model]
60
+ end
61
+
62
+ def city_model
63
+ options[:city_model]
64
+ end
65
+
66
+ def barangay_model
67
+ options[:barangay_model]
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "psgc"
4
+
5
+ # Auto-generated seed file for PSGC geographic data
6
+ # Run with: rails db:seed
7
+ #
8
+ # Model names can be customized via generator options:
9
+ # rails g psgc:seed --region-model=Address::Region --province-model=PsgcProvince
10
+
11
+ <% if level == "all" || level == "regions" -%>
12
+ # Regions (<%= Psgc.regions.length %>)
13
+ <%= region_model %>.upsert_all(Psgc.regions, unique_by: :code)
14
+ <% end -%>
15
+
16
+ <% if level == "all" || level == "provinces" -%>
17
+ # Provinces (<%= Psgc.provinces.length %>)
18
+ <%= province_model %>.upsert_all(Psgc.provinces, unique_by: :code)
19
+ <% end -%>
20
+
21
+ <% if level == "all" || level == "cities" || level == "cities_municipalities" -%>
22
+ # Cities/Municipalities (<%= Psgc.cities_municipalities.length %>)
23
+ <%= city_model %>.upsert_all(Psgc.cities_municipalities, unique_by: :code)
24
+ <% end -%>
25
+
26
+ <% if level == "all" || level == "barangays" -%>
27
+ # Barangays (<%= Psgc.barangays.length %>)
28
+ <%= barangay_model %>.upsert_all(Psgc.barangays, unique_by: :code)
29
+ <% end -%>
30
+
31
+ puts "PSGC seed data loaded successfully"
data/lib/psgc/cli.rb ADDED
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module Psgc
6
+ class CLI
7
+ COMMANDS = %w[find hierarchy valid stats export help].freeze
8
+
9
+ def self.run(argv = ARGV)
10
+ new(argv).run
11
+ end
12
+
13
+ def initialize(argv)
14
+ @argv = argv.dup
15
+ @options = {}
16
+ @success = false
17
+ end
18
+
19
+ def run
20
+ return help if @argv.empty?
21
+
22
+ parse_options
23
+
24
+ unless @command && COMMANDS.include?(@command)
25
+ $stderr.puts "Error: unknown command '#{@command}'"
26
+ return false
27
+ end
28
+
29
+ send(@command)
30
+ @success
31
+ end
32
+
33
+ private
34
+
35
+ def parse_options
36
+ @options[:limit] = 20
37
+ @options[:export_format] = :csv
38
+ @options[:export_level] = :regions
39
+
40
+ begin
41
+ OptionParser.new do |opts|
42
+ opts.on("-h", "--help") { @options[:help] = true }
43
+ opts.on("--version") { @options[:version] = true }
44
+ opts.on("--limit=N", Integer) { |n| @options[:limit] = n }
45
+ opts.on("--csv") { @options[:export_format] = :csv }
46
+ opts.on("--yaml") { @options[:export_format] = :yaml }
47
+ opts.on("--geojson") { @options[:export_format] = :geojson }
48
+ opts.on("--level=L") { |l| @options[:export_level] = l.to_sym }
49
+ end.parse!(@argv)
50
+ rescue OptionParser::InvalidOption => e
51
+ $stderr.puts "Error: #{e.message}"
52
+ @command = nil
53
+ return
54
+ end
55
+
56
+ @command = @argv.shift
57
+ @command ||= "help"
58
+ end
59
+
60
+ def find
61
+ return help if @options[:help]
62
+ return version if @options[:version]
63
+ return help_find if @argv.empty?
64
+
65
+ query = @argv.join(" ")
66
+ results = Psgc.search(query, limit: @options[:limit])
67
+
68
+ has_results = false
69
+ [:regions, :provinces, :cities_municipalities, :barangays].each do |level|
70
+ items = results[level]
71
+ next unless items && !items.empty?
72
+
73
+ has_results = true
74
+ puts "=== #{level.to_s.tr('_', ' ').capitalize} (#{items.length}) ==="
75
+ items.each do |item|
76
+ puts " #{item[:code]} #{item[:name]}"
77
+ end
78
+ end
79
+
80
+ @success = has_results
81
+ help_find unless has_results
82
+ end
83
+
84
+ def help_find
85
+ puts "Usage: psgc find <query> [options]"
86
+ puts " Fuzzy search across all geographic levels."
87
+ puts " --limit=N Limit results per level (default: 20)"
88
+ puts ""
89
+ puts "Examples:"
90
+ puts " psgc find Manila"
91
+ puts " psgc find Cebu --limit 10"
92
+ @success = false
93
+ end
94
+
95
+ def hierarchy
96
+ return help if @options[:help]
97
+ return version if @options[:version]
98
+ return help_hierarchy if @argv.empty?
99
+
100
+ code = @argv.first
101
+ unless code && code.match?(/^\d{10}$/)
102
+ $stderr.puts "Error: code must be exactly 10 digits"
103
+ @success = false
104
+ return
105
+ end
106
+
107
+ result = Psgc.hierarchy(code)
108
+ return help_hierarchy("Invalid code") unless result && result.any? { |k, v| k != :code && v }
109
+
110
+ puts "Code: #{result[:code]}"
111
+
112
+ if result[:region]
113
+ puts "Region: #{result[:region][:code]} #{result[:region][:name]}"
114
+ end
115
+ if result[:province]
116
+ puts "Province: #{result[:province][:code]} #{result[:province][:name]}"
117
+ end
118
+ if result[:city_municipality]
119
+ puts "City/Municipality: #{result[:city_municipality][:code]} #{result[:city_municipality][:name]}"
120
+ end
121
+ if result[:barangay]
122
+ puts "Barangay: #{result[:barangay][:code]} #{result[:barangay][:name]}"
123
+ end
124
+
125
+ @success = true
126
+ end
127
+
128
+ def help_hierarchy(msg = nil)
129
+ puts "Usage: psgc hierarchy <code>"
130
+ puts " Show full hierarchy for a PSGC code."
131
+ puts ""
132
+ puts "Examples:"
133
+ puts " psgc hierarchy 1378040012"
134
+ puts " psgc hierarchy 1400000000"
135
+ $stderr.puts msg if msg
136
+ @success = false
137
+ end
138
+
139
+ def valid
140
+ return help if @options[:help]
141
+ return version if @options[:version]
142
+ return help_valid if @argv.empty?
143
+
144
+ code = @argv.first
145
+ valid = Psgc.valid?(code)
146
+
147
+ if valid
148
+ puts "Valid PSGC code: #{code}"
149
+ else
150
+ puts "Invalid PSGC code: #{code}"
151
+ end
152
+ @success = valid
153
+ end
154
+
155
+ def help_valid
156
+ puts "Usage: psgc valid <code>"
157
+ puts " Check if PSGC code exists."
158
+ puts ""
159
+ puts "Examples:"
160
+ puts " psgc valid 1378040012"
161
+ puts " psgc valid 9999999999"
162
+ @success = false
163
+ end
164
+
165
+ def stats
166
+ return help if @options[:help]
167
+ return version if @options[:version]
168
+
169
+ result = Psgc.stats
170
+ printf "%-22s %d\n", "Regions:", result[:regions]
171
+ printf "%-22s %d\n", "Provinces:", result[:provinces]
172
+ printf "%-22s %d\n", "Cities/Municipalities:", result[:cities_municipalities]
173
+ printf "%-22s %d\n", "Barangays:", result[:barangays]
174
+
175
+ @success = true
176
+ end
177
+
178
+ def export
179
+ return help if @options[:help]
180
+ return version if @options[:version]
181
+
182
+ format = @options[:export_format]
183
+ level = @options[:export_level]
184
+
185
+ begin
186
+ case format
187
+ when :csv
188
+ puts Psgc.export_csv(level: level)
189
+ when :yaml
190
+ puts Psgc.export_yaml(level: level)
191
+ when :geojson
192
+ puts Psgc.export_geojson(level: level)
193
+ end
194
+ rescue ArgumentError => e
195
+ $stderr.puts "Error: #{e.message}"
196
+ @success = false
197
+ return
198
+ end
199
+
200
+ @success = true
201
+ end
202
+
203
+ def help
204
+ puts "Usage: psgc <command> [options]"
205
+ puts ""
206
+ puts "Commands:"
207
+ puts " find <query> Fuzzy search across all geographic levels"
208
+ puts " hierarchy <code> Show full hierarchy for a PSGC code"
209
+ puts " valid <code> Check if PSGC code exists"
210
+ puts " stats Show statistics"
211
+ puts " export Export data in various formats (CSV, YAML, GeoJSON)"
212
+ puts ""
213
+ puts "Run 'psgc <command> --help' for more details."
214
+ @success = true
215
+ end
216
+
217
+ def version
218
+ puts "psgc-rb #{Psgc::VERSION}"
219
+ @success = true
220
+ end
221
+ end
222
+ end
data/lib/psgc.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  require "json"
4
4
 
5
5
  module Psgc
6
- VERSION = "0.1.0"
6
+ VERSION = "0.2.0"
7
7
 
8
8
  class Error < StandardError; end
9
9
 
@@ -11,20 +11,22 @@ module Psgc
11
11
  File.expand_path("../../data", __FILE__)
12
12
  end
13
13
 
14
+ @data_mutex = Mutex.new
15
+
14
16
  def self.regions
15
- @regions ||= load_data("regions")
17
+ @regions || @data_mutex.synchronize { @regions ||= load_data("regions") }
16
18
  end
17
19
 
18
20
  def self.provinces
19
- @provinces ||= load_data("provinces")
21
+ @provinces || @data_mutex.synchronize { @provinces ||= load_data("provinces") }
20
22
  end
21
23
 
22
24
  def self.cities_municipalities
23
- @cities_municipalities ||= load_data("cities_municipalities")
25
+ @cities_municipalities || @data_mutex.synchronize { @cities_municipalities ||= load_data("cities_municipalities") }
24
26
  end
25
27
 
26
28
  def self.barangays
27
- @barangays ||= load_data("barangays")
29
+ @barangays || @data_mutex.synchronize { @barangays ||= load_data("barangays") }
28
30
  end
29
31
 
30
32
  def self.load_data(type)
@@ -33,4 +35,215 @@ module Psgc
33
35
 
34
36
  JSON.parse(File.read(file_path), symbolize_names: true)
35
37
  end
36
- end
38
+
39
+ def self.find_region(code: nil, name: nil)
40
+ return nil unless code || name
41
+ find(regions, code: code, name: name)
42
+ end
43
+
44
+ def self.find_province(code: nil, name: nil, region_code: nil)
45
+ return nil unless code || name || region_code
46
+ find(provinces, code: code, name: name, region_code: region_code)
47
+ end
48
+
49
+ def self.find_city_municipality(code: nil, name: nil, province_code: nil)
50
+ return nil unless code || name || province_code
51
+ find(cities_municipalities, code: code, name: name, province_code: province_code)
52
+ end
53
+
54
+ def self.find_barangay(code: nil, name: nil, city_municipality_code: nil)
55
+ return nil unless code || name || city_municipality_code
56
+ find(barangays, code: code, name: name, city_municipality_code: city_municipality_code)
57
+ end
58
+
59
+ # Finds first match in collection using AND semantics.
60
+ # All non-nil criteria must match for a result.
61
+ # Name matching is case-insensitive substring.
62
+ # Code attributes (*_code) use bidirectional prefix matching:
63
+ # "v.start_with?(item[k]) || item[k].start_with?(v)"
64
+ # e.g., region_code "14" matches stored "1400000000" and vice versa.
65
+ #
66
+ # @param collection [Array<Hash>] data collection
67
+ # @param code [String, nil] exact PSGC code
68
+ # @param name [String, nil] case-insensitive substring match
69
+ # @param attrs [Hash] additional match criteria
70
+ # @return [Hash, nil] first matching item or nil
71
+ def self.find(collection, code: nil, name: nil, **attrs)
72
+ collection.each do |item|
73
+ matches = true
74
+ matches &&= item[:code] == code if code
75
+ matches &&= item[:name].to_s.downcase.include?(name.to_s.downcase) if name
76
+ attrs.each do |k, v|
77
+ next unless v
78
+ if k.to_s.end_with?("_code")
79
+ matches &&= v.to_s.start_with?(item[k].to_s) || item[k].to_s.start_with?(v.to_s)
80
+ else
81
+ matches &&= item[k] == v
82
+ end
83
+ end
84
+ return item if matches
85
+ end
86
+ nil
87
+ end
88
+
89
+ # Traverses PSGC hierarchy for a given code.
90
+ # Uses prefix matching because parent codes are prefixes of child codes
91
+ # (e.g., city code is prefix of barangay code).
92
+ #
93
+ # @param code [String, Integer] 10-digit PSGC code
94
+ # @return [Hash{Symbol => Hash, nil}] hash with :code and found geographic levels
95
+ def self.hierarchy(code)
96
+ return nil unless code.to_s.match?(/^\d{10}$/)
97
+ code = code.to_s
98
+
99
+ result = { code: code }
100
+
101
+ if code.end_with?("000000")
102
+ result[:region] = find_region(code: code)
103
+ elsif code.end_with?("0000")
104
+ result[:province] = find_province(code: code)
105
+ result[:city_municipality] = find_city_municipality(code: code) unless result[:province]
106
+ elsif code.end_with?("00")
107
+ result[:city_municipality] = find_city_municipality(code: code)
108
+ else
109
+ result[:barangay] = find_barangay(code: code)
110
+ end
111
+
112
+ if result[:barangay]
113
+ city = cities_municipalities.find { |c| c[:code].start_with?(result[:barangay][:city_municipality_code]) }
114
+ result[:city_municipality] = city
115
+ if city
116
+ province = provinces.find { |p| p[:code].start_with?(city[:province_code]) }
117
+ result[:province] = province
118
+ end
119
+ elsif result[:city_municipality]
120
+ province = provinces.find { |p| p[:code].start_with?(result[:city_municipality][:province_code]) }
121
+ result[:province] = province
122
+ end
123
+
124
+ if result[:province]
125
+ region = regions.find { |r| r[:code].start_with?(result[:province][:region_code]) }
126
+ result[:region] = region
127
+ end
128
+
129
+ result[:region] ||= regions.find { |r| r[:code].start_with?(code[0, 2]) }
130
+
131
+ result
132
+ end
133
+
134
+ # @param query [String] search term (case-insensitive substring match)
135
+ # @param levels [Array<Symbol>] which levels to search (:regions, :provinces, :cities, :cities_municipalities, :barangays)
136
+ # @param limit [Integer] max matches per level (not total)
137
+ # @return [Hash{Symbol => Array}] hash with requested level keys and matching records
138
+ def self.search(query, levels: nil, limit: nil)
139
+ return {} unless query && !query.to_s.strip.empty?
140
+
141
+ query_down = query.to_s.downcase
142
+ levels ||= [:regions, :provinces, :cities_municipalities, :barangays]
143
+
144
+ result = {}
145
+ levels.each do |level|
146
+ collection = collection_for(level)
147
+
148
+ matches = collection.select { |item| item[:name].to_s.downcase.include?(query_down) }
149
+ matches = matches.first(limit) if limit
150
+ result[level] = matches
151
+ end
152
+
153
+ result
154
+ end
155
+
156
+ # @param code [String, Integer] 10-digit PSGC code
157
+ # @return [Boolean] true if code exists in any level
158
+ def self.valid?(code)
159
+ return false unless code && code.to_s.match?(/^\d{10}$/)
160
+
161
+ code_str = code.to_s
162
+
163
+ if code_str.end_with?("000000")
164
+ regions.any? { |r| r[:code] == code_str }
165
+ elsif code_str.end_with?("0000")
166
+ provinces.any? { |p| p[:code] == code_str } ||
167
+ cities_municipalities.any? { |c| c[:code] == code_str }
168
+ elsif code_str.end_with?("00")
169
+ cities_municipalities.any? { |c| c[:code] == code_str } ||
170
+ barangays.any? { |b| b[:code] == code_str }
171
+ else
172
+ barangays.any? { |b| b[:code] == code_str }
173
+ end
174
+ end
175
+
176
+ # @return [Hash{Symbol => Integer}] counts by level
177
+ def self.stats
178
+ {
179
+ regions: regions.length,
180
+ provinces: provinces.length,
181
+ cities_municipalities: cities_municipalities.length,
182
+ barangays: barangays.length
183
+ }
184
+ end
185
+
186
+ # @param level [Symbol] geographic level
187
+ # @return [Symbol] normalized level (:cities -> :cities_municipalities)
188
+ def self.normalize_level(level)
189
+ case level
190
+ when :cities then :cities_municipalities
191
+ else level
192
+ end
193
+ end
194
+
195
+ def self.collection_for(level)
196
+ normalized = normalize_level(level)
197
+
198
+ case normalized
199
+ when :regions then regions
200
+ when :provinces then provinces
201
+ when :cities_municipalities then cities_municipalities
202
+ when :barangays then barangays
203
+ else raise ArgumentError, "unknown level: #{level}. Valid: :regions, :provinces, :cities, :cities_municipalities, :barangays"
204
+ end
205
+ end
206
+
207
+ def self.export_csv(level: :regions, include_headers: true)
208
+ require "csv"
209
+ collection = collection_for(level)
210
+
211
+ headers = collection.first.keys.map(&:to_s)
212
+
213
+ CSV.generate do |csv|
214
+ csv << headers if include_headers
215
+ collection.each do |item|
216
+ csv << headers.map { |h| item[h.to_sym] }
217
+ end
218
+ end
219
+ end
220
+
221
+ def self.export_yaml(level: :regions)
222
+ require "yaml"
223
+ collection = collection_for(level)
224
+
225
+ { normalize_level(level) => collection }.to_yaml
226
+ end
227
+
228
+ # @return [String] GeoJSON FeatureCollection
229
+ # Note: geometry is null because PSGC data has no geographic coordinates.
230
+ def self.export_geojson(level: :regions)
231
+ collection = collection_for(level)
232
+
233
+ features = collection.map do |item|
234
+ props = item.dup
235
+ props.delete(:code)
236
+ {
237
+ type: "Feature",
238
+ id: item[:code],
239
+ geometry: nil,
240
+ properties: props
241
+ }
242
+ end
243
+
244
+ {
245
+ type: "FeatureCollection",
246
+ features: features
247
+ }.to_json
248
+ end
249
+ end
metadata CHANGED
@@ -1,23 +1,53 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: psgc-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Al Kevin Tan
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: csv
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 2.0.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 2.0.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: psych
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 4.0.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 4.0.0
12
40
  description: A Ruby gem providing up-to-date Philippine geographic data from the PSA.
13
41
  Includes all regions, provinces, cities, municipalities, and barangays with PSGC
14
42
  codes.
15
43
  email:
16
44
  - alkevintan@gmail.com
17
- executables: []
45
+ executables:
46
+ - psgc
18
47
  extensions: []
19
48
  extra_rdoc_files: []
20
49
  files:
50
+ - CHANGELOG.md
21
51
  - CONTRIBUTING.md
22
52
  - LICENSE
23
53
  - README.md
@@ -26,13 +56,17 @@ files:
26
56
  - data/cities_municipalities.json
27
57
  - data/provinces.json
28
58
  - data/regions.json
59
+ - exe/psgc
60
+ - lib/generators/psgc/seed_generator.rb
61
+ - lib/generators/psgc/templates/seed.rb.erb
29
62
  - lib/psgc.rb
63
+ - lib/psgc/cli.rb
30
64
  homepage: https://github.com/alkevintan/psgc-rb
31
65
  licenses: []
32
66
  metadata:
33
67
  allowed_push_host: https://rubygems.org
34
68
  homepage_uri: https://github.com/alkevintan/psgc-rb
35
- source_code_uri: https://github.com/alkevintan/psgc-rb
69
+ source_code_uri: https://github.com/alkevintan/psgc-rb/tree/v0.2.0
36
70
  rdoc_options: []
37
71
  require_paths:
38
72
  - lib