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 +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +93 -1
- data/exe/psgc +8 -0
- data/lib/generators/psgc/seed_generator.rb +71 -0
- data/lib/generators/psgc/templates/seed.rb.erb +31 -0
- data/lib/psgc/cli.rb +222 -0
- data/lib/psgc.rb +219 -6
- metadata +38 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a1bfa699725cdd1d7ab90c3086d4fa00206eba2fa8bf588a305f9a3c114f9e94
|
|
4
|
+
data.tar.gz: e58c17f37ca5d80825539bc410cc53725491a1fb1c27a278bab6cef540f9b1f1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
148
|
+
Released under the [MIT License](./LICENSE).
|
data/exe/psgc
ADDED
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
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
|