has_geo_lookup 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.
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Administrative boundary geometries from GeoBoundaries.org
4
+ #
5
+ # This model stores precise administrative boundary polygons from GeoBoundaries.org,
6
+ # providing accurate geometric shapes for countries, states, counties, and municipalities
7
+ # worldwide. Each boundary includes PostGIS geometry data for spatial queries and
8
+ # containment testing.
9
+ #
10
+ # GeoBoundaries provides multiple administrative levels:
11
+ # - ADM0: Country boundaries
12
+ # - ADM1: State/province boundaries (e.g., California, Ontario)
13
+ # - ADM2: County/district boundaries (e.g., Los Angeles County)
14
+ # - ADM3: Municipality boundaries (e.g., city limits)
15
+ # - ADM4: Neighborhood/ward boundaries
16
+ #
17
+ # The boundary geometries are stored using PostGIS and can be used for precise
18
+ # point-in-polygon queries to determine administrative containment.
19
+ #
20
+ # @attr [String] name Official boundary name
21
+ # @attr [String] level Administrative level (ADM0, ADM1, ADM2, ADM3, ADM4)
22
+ # @attr [String] shape_iso ISO3 country code for this boundary
23
+ # @attr [String] shape_group Grouping identifier for related boundaries
24
+ # @attr [RGeo::Geos::CAPIGeometryMethods] boundary PostGIS geometry (polygon/multipolygon)
25
+ #
26
+ # @example Find boundaries containing a point
27
+ # Geoboundary.where("ST_Contains(boundary, ST_GeomFromText('POINT(-74.0060 40.7128)', 4326))")
28
+ #
29
+ # @example Find state-level boundaries for the US
30
+ # Geoboundary.where(level: "ADM1").where("shape_iso LIKE '%USA%'")
31
+ #
32
+ # @see https://www.geoboundaries.org GeoBoundaries.org data source
33
+ class Geoboundary < ActiveRecord::Base
34
+ # Associations
35
+ has_and_belongs_to_many :metros
36
+
37
+ # Validations
38
+ validates :name, presence: true
39
+ validates :level, presence: true, inclusion: { in: %w[ADM0 ADM1 ADM2 ADM3 ADM4] }
40
+ validates :boundary, presence: true
41
+
42
+ # Scopes for different administrative levels
43
+ scope :countries, -> { where(level: "ADM0") }
44
+ scope :states_provinces, -> { where(level: "ADM1") }
45
+ scope :counties_districts, -> { where(level: "ADM2") }
46
+ scope :municipalities, -> { where(level: "ADM3") }
47
+ scope :neighborhoods, -> { where(level: "ADM4") }
48
+
49
+ scope :by_country, ->(country_code) {
50
+ iso3 = country_code_to_iso3(country_code)
51
+ where("shape_iso LIKE ? OR shape_group LIKE ?", "%#{iso3}%", "%#{iso3}%") if iso3
52
+ }
53
+
54
+ scope :containing_point, ->(latitude, longitude) {
55
+ where("ST_Contains(boundary, ST_GeomFromText(?, 4326))", "POINT(#{longitude} #{latitude})")
56
+ }
57
+
58
+ # Check if this boundary contains the given coordinates
59
+ #
60
+ # Uses PostGIS ST_Contains function to perform precise geometric containment
61
+ # testing against the boundary polygon.
62
+ #
63
+ # @param latitude [Float] Latitude in decimal degrees
64
+ # @param longitude [Float] Longitude in decimal degrees
65
+ # @return [Boolean] true if the point is within this boundary
66
+ #
67
+ # @example
68
+ # boundary.contains_point?(40.7128, -74.0060)
69
+ # # => true (if NYC coordinates are within this boundary)
70
+ def contains_point?(latitude, longitude)
71
+ return false unless latitude && longitude && boundary
72
+
73
+ point_wkt = "POINT(#{longitude} #{latitude})"
74
+
75
+ self.class.connection.select_value(
76
+ "SELECT ST_Contains(ST_GeomFromText(?), ST_GeomFromText(?, 4326)) AS contains",
77
+ boundary.to_s, point_wkt
78
+ ) == 1
79
+ rescue => e
80
+ Rails.logger.warn "Error checking point containment: #{e.message}"
81
+ false
82
+ end
83
+
84
+ # Calculate the area of this boundary in square kilometers
85
+ #
86
+ # Uses PostGIS ST_Area function with spheroid calculations for accurate
87
+ # area computation on the Earth's surface.
88
+ #
89
+ # @return [Float] Area in square kilometers
90
+ #
91
+ # @example
92
+ # boundary.area_km2
93
+ # # => 10991.5 (area in square kilometers)
94
+ def area_km2
95
+ return nil unless boundary
96
+
97
+ area_m2 = self.class.connection.select_value(
98
+ "SELECT ST_Area(ST_Transform(ST_GeomFromText(?), 3857))",
99
+ boundary.to_s
100
+ )
101
+
102
+ area_m2 ? (area_m2 / 1_000_000).round(2) : nil
103
+ rescue => e
104
+ Rails.logger.warn "Error calculating boundary area: #{e.message}"
105
+ nil
106
+ end
107
+
108
+ # Get the centroid (center point) of this boundary
109
+ #
110
+ # Uses PostGIS ST_Centroid function to find the geometric center
111
+ # of the boundary polygon.
112
+ #
113
+ # @return [Array<Float>, nil] [latitude, longitude] of centroid, or nil if error
114
+ #
115
+ # @example
116
+ # boundary.centroid
117
+ # # => [40.7589, -73.9851] (lat, lng of boundary center)
118
+ def centroid
119
+ return nil unless boundary
120
+
121
+ result = self.class.connection.select_one(
122
+ "SELECT ST_Y(ST_Centroid(ST_GeomFromText(?))) AS lat, ST_X(ST_Centroid(ST_GeomFromText(?))) AS lng",
123
+ boundary.to_s, boundary.to_s
124
+ )
125
+
126
+ result ? [result['lat'].to_f, result['lng'].to_f] : nil
127
+ rescue => e
128
+ Rails.logger.warn "Error calculating boundary centroid: #{e.message}"
129
+ nil
130
+ end
131
+
132
+ # Returns a human-readable description of this boundary
133
+ #
134
+ # Includes the boundary name, administrative level, and country context
135
+ # for clear identification.
136
+ #
137
+ # @return [String] Formatted description
138
+ #
139
+ # @example
140
+ # boundary.display_name
141
+ # # => "Los Angeles County (ADM2 - County/District, USA)"
142
+ def display_name
143
+ level_description = case level
144
+ when "ADM0" then "Country"
145
+ when "ADM1" then "State/Province"
146
+ when "ADM2" then "County/District"
147
+ when "ADM3" then "Municipality"
148
+ when "ADM4" then "Neighborhood/Ward"
149
+ else level
150
+ end
151
+
152
+ country_info = extract_country_from_shape_iso
153
+ country_suffix = country_info ? ", #{country_info}" : ""
154
+
155
+ "#{name} (#{level} - #{level_description}#{country_suffix})"
156
+ end
157
+
158
+ # Check if this is a country-level boundary
159
+ # @return [Boolean] true if level is "ADM0"
160
+ def country?
161
+ level == "ADM0"
162
+ end
163
+
164
+ # Check if this is a state/province-level boundary
165
+ # @return [Boolean] true if level is "ADM1"
166
+ def state_province?
167
+ level == "ADM1"
168
+ end
169
+
170
+ # Check if this is a county/district-level boundary
171
+ # @return [Boolean] true if level is "ADM2"
172
+ def county_district?
173
+ level == "ADM2"
174
+ end
175
+
176
+ # Check if this is a municipality-level boundary
177
+ # @return [Boolean] true if level is "ADM3"
178
+ def municipality?
179
+ level == "ADM3"
180
+ end
181
+
182
+ # Check if this is a neighborhood-level boundary
183
+ # @return [Boolean] true if level is "ADM4"
184
+ def neighborhood?
185
+ level == "ADM4"
186
+ end
187
+
188
+ private
189
+
190
+ # Convert 2-letter country code to 3-letter ISO3 code
191
+ def self.country_code_to_iso3(country_code)
192
+ return nil unless country_code&.length == 2
193
+
194
+ begin
195
+ country = Iso3166.for_code(country_code.upcase)
196
+ country&.code3
197
+ rescue
198
+ nil
199
+ end
200
+ end
201
+
202
+ # Extract country information from shape_iso field
203
+ def extract_country_from_shape_iso
204
+ return nil unless shape_iso.present?
205
+
206
+ # shape_iso often contains patterns like "USA-ADM1" or similar
207
+ # Extract the country code portion
208
+ country_match = shape_iso.match(/([A-Z]{3})/)
209
+ return nil unless country_match
210
+
211
+ iso3_code = country_match[1]
212
+
213
+ begin
214
+ country = Iso3166.for_alpha3(iso3_code)
215
+ country&.name
216
+ rescue
217
+ iso3_code
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Geoname model for geographic place names from Geonames.org
4
+ #
5
+ # This model represents geographic features from the Geonames.org dataset, which provides
6
+ # comprehensive information about populated places, administrative divisions, and geographic
7
+ # features worldwide. Each record includes coordinates, names, administrative codes, and
8
+ # feature classifications.
9
+ #
10
+ # The Geonames dataset is organized using feature classes and codes that categorize
11
+ # different types of geographic entities (populated places, administrative areas,
12
+ # hydrographic features, etc.).
13
+ #
14
+ # @example Find populated places in the US
15
+ # Geoname.where(country_code: "US", feature_class: "P")
16
+ #
17
+ # @example Find administrative divisions
18
+ # Geoname.where(feature_class: "A", feature_code: "ADM2")
19
+ #
20
+ # @see https://www.geonames.org/ Official Geonames website
21
+ # @see FeatureCode For feature classification definitions
22
+ class Geoname < ActiveRecord::Base
23
+ include HasGeoLookup
24
+
25
+ # Associations
26
+ has_and_belongs_to_many :metros
27
+
28
+ # Validations
29
+ validates :name, presence: true
30
+ validates :latitude, :longitude, presence: true, numericality: true
31
+ validates :feature_class, presence: true, length: { is: 1 }
32
+ validates :feature_code, presence: true, length: { maximum: 10 }
33
+ validates :country_code, length: { is: 2 }, allow_blank: true
34
+ validates :population, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
35
+ validates :elevation, numericality: true, allow_nil: true
36
+
37
+ # Scopes for common queries
38
+ scope :populated_places, -> { where(feature_class: "P") }
39
+ scope :administrative, -> { where(feature_class: "A") }
40
+ scope :hydrographic, -> { where(feature_class: "H") }
41
+ scope :terrain, -> { where(feature_class: "T") }
42
+ scope :roads_railways, -> { where(feature_class: "R") }
43
+ scope :spots_buildings, -> { where(feature_class: "S") }
44
+ scope :undersea, -> { where(feature_class: "U") }
45
+ scope :vegetation, -> { where(feature_class: "V") }
46
+
47
+ scope :by_country, ->(country_code) { where(country_code: country_code.upcase) }
48
+ scope :with_population, -> { where.not(population: [nil, 0]) }
49
+ scope :major_places, -> { with_population.where("population > ?", 100000) }
50
+
51
+ # Geographic bounds scopes
52
+ scope :within_bounds, ->(north, south, east, west) {
53
+ where(latitude: south..north, longitude: west..east)
54
+ }
55
+
56
+ scope :near_coordinates, ->(lat, lng, radius_deg = 1.0) {
57
+ where(
58
+ latitude: (lat - radius_deg)..(lat + radius_deg),
59
+ longitude: (lng - radius_deg)..(lng + radius_deg)
60
+ )
61
+ }
62
+
63
+ # Calculate distance to coordinates using Haversine formula
64
+ #
65
+ # This method calculates the great-circle distance between this geoname's coordinates
66
+ # and the provided coordinates using the Haversine formula. Useful for finding nearby
67
+ # places or sorting by distance.
68
+ #
69
+ # @param target_lat [Float] Target latitude in degrees
70
+ # @param target_lng [Float] Target longitude in degrees
71
+ # @return [Float] Distance in kilometers
72
+ #
73
+ # @example
74
+ # geoname.distance_to(40.7128, -74.0060)
75
+ # # => 245.67 (distance in kilometers)
76
+ def distance_to(target_lat, target_lng)
77
+ return nil unless latitude && longitude && target_lat && target_lng
78
+
79
+ # Haversine formula
80
+ radius_km = 6371.0
81
+ lat1_rad = latitude * Math::PI / 180
82
+ lat2_rad = target_lat * Math::PI / 180
83
+ delta_lat_rad = (target_lat - latitude) * Math::PI / 180
84
+ delta_lng_rad = (target_lng - longitude) * Math::PI / 180
85
+
86
+ a = Math.sin(delta_lat_rad / 2) * Math.sin(delta_lat_rad / 2) +
87
+ Math.cos(lat1_rad) * Math.cos(lat2_rad) *
88
+ Math.sin(delta_lng_rad / 2) * Math.sin(delta_lng_rad / 2)
89
+
90
+ c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
91
+ radius_km * c
92
+ end
93
+
94
+ # Returns a human-readable description of this place
95
+ #
96
+ # Combines the name with administrative context and country information to provide
97
+ # a comprehensive description suitable for display or logging.
98
+ #
99
+ # @return [String] Formatted description
100
+ #
101
+ # @example
102
+ # geoname.display_name
103
+ # # => "New York, New York, US (PPL - populated place)"
104
+ def display_name
105
+ parts = [name]
106
+ parts << admin1_name if admin1_name.present?
107
+ parts << admin2_name if admin2_name.present? && admin2_name != admin1_name
108
+ parts << country_code if country_code.present?
109
+
110
+ description = parts.join(", ")
111
+
112
+ if feature_code.present?
113
+ description += " (#{feature_code})"
114
+ end
115
+
116
+ description
117
+ end
118
+
119
+ # Check if this is a populated place
120
+ # @return [Boolean] true if feature_class is "P"
121
+ def populated_place?
122
+ feature_class == "P"
123
+ end
124
+
125
+ # Check if this is an administrative division
126
+ # @return [Boolean] true if feature_class is "A"
127
+ def administrative_division?
128
+ feature_class == "A"
129
+ end
130
+
131
+ # Get the administrative level for administrative divisions
132
+ # @return [String, nil] Administrative level (ADM1, ADM2, etc.) or nil
133
+ def administrative_level
134
+ return nil unless administrative_division?
135
+ feature_code if feature_code&.start_with?("ADM")
136
+ end
137
+
138
+ # Ensure country codes are stored in uppercase
139
+ def country_code=(value)
140
+ super(value&.upcase)
141
+ end
142
+
143
+ # Ensure feature class is stored in uppercase
144
+ def feature_class=(value)
145
+ super(value&.upcase)
146
+ end
147
+
148
+ # Ensure feature code is stored in uppercase
149
+ def feature_code=(value)
150
+ super(value&.upcase)
151
+ end
152
+ end
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Metropolitan areas defined by collections of administrative boundaries
4
+ #
5
+ # This model represents metropolitan areas (metro areas, urban agglomerations) as
6
+ # collections of administrative boundaries from GeoBoundaries.org. Rather than storing
7
+ # separate geometry, metros are defined by their constituent geoboundaries, allowing
8
+ # for flexible and maintainable metro definitions.
9
+ #
10
+ # Metro areas are useful for:
11
+ # - Real estate market analysis by metropolitan region
12
+ # - Economic data aggregation across municipal boundaries
13
+ # - Transportation and infrastructure planning
14
+ # - Population and demographic analysis
15
+ #
16
+ # Each metro can span multiple administrative levels and boundaries, reflecting
17
+ # the real-world nature of metropolitan areas that often cross county, city,
18
+ # and sometimes state boundaries.
19
+ #
20
+ # @attr [String] name Metro area name (e.g., "San Francisco Bay Area", "Greater London")
21
+ # @attr [String] details Additional descriptive information about the metro
22
+ # @attr [String] country_code Primary country code for this metro area
23
+ # @attr [Integer] population Estimated metro population (optional)
24
+ #
25
+ # @example Find metros containing a point
26
+ # Metro.joins(:geoboundaries)
27
+ # .where("ST_Contains(geoboundaries.boundary, ST_GeomFromText(?, 4326))", "POINT(-122.4194 37.7749)")
28
+ #
29
+ # @example Find metros in a specific country
30
+ # Metro.where(country_code: "US")
31
+ class Metro < ActiveRecord::Base
32
+ # Associations
33
+ has_and_belongs_to_many :geoboundaries
34
+
35
+ # Validations
36
+ validates :name, presence: true, uniqueness: true
37
+ validates :country_code, length: { is: 2 }, allow_blank: true
38
+ validates :population, numericality: { greater_than: 0 }, allow_nil: true
39
+
40
+ # Scopes
41
+ scope :by_country, ->(country_code) { where(country_code: country_code.upcase) }
42
+ scope :with_population, -> { where.not(population: nil) }
43
+ scope :major_metros, -> { with_population.where("population > ?", 1_000_000) }
44
+
45
+ scope :containing_point, ->(latitude, longitude) {
46
+ joins(:geoboundaries)
47
+ .where("ST_Contains(geoboundaries.boundary, ST_GeomFromText(?, 4326))",
48
+ "POINT(#{longitude} #{latitude})")
49
+ .distinct
50
+ }
51
+
52
+ # Check if this metro contains the given coordinates
53
+ #
54
+ # Uses PostGIS spatial queries against all associated geoboundaries to determine
55
+ # if the point falls within any boundary that defines this metropolitan area.
56
+ #
57
+ # @param latitude [Float] Latitude in decimal degrees
58
+ # @param longitude [Float] Longitude in decimal degrees
59
+ # @return [Boolean] true if the point is within this metro area
60
+ #
61
+ # @example
62
+ # metro.contains_point?(37.7749, -122.4194)
63
+ # # => true (if coordinates are within San Francisco Bay Area)
64
+ def contains_point?(latitude, longitude)
65
+ return false unless latitude && longitude
66
+ return false unless geoboundaries.any?
67
+
68
+ # Check if point is contained within any of the metro's boundaries
69
+ geoboundaries.joins("INNER JOIN geoboundaries gb ON gb.id = geoboundaries.id")
70
+ .where("ST_Contains(gb.boundary, ST_GeomFromText(?, 4326))",
71
+ "POINT(#{longitude} #{latitude})")
72
+ .exists?
73
+ rescue => e
74
+ Rails.logger.warn "Error checking metro point containment: #{e.message}"
75
+ false
76
+ end
77
+
78
+ # Calculate the total area of this metro in square kilometers
79
+ #
80
+ # Sums the areas of all constituent geoboundaries, with handling for
81
+ # overlapping boundaries to avoid double-counting.
82
+ #
83
+ # @return [Float, nil] Total area in square kilometers, or nil if no boundaries
84
+ #
85
+ # @example
86
+ # metro.total_area_km2
87
+ # # => 18040.5 (total metro area in square kilometers)
88
+ def total_area_km2
89
+ return nil unless geoboundaries.any?
90
+
91
+ # Calculate total area using ST_Union to handle overlapping boundaries
92
+ result = self.class.connection.select_value(<<~SQL.squish)
93
+ SELECT ST_Area(ST_Transform(ST_Union(boundary), 3857)) / 1000000 AS total_area_km2
94
+ FROM geoboundaries
95
+ WHERE id IN (#{geoboundary_ids.join(',')})
96
+ SQL
97
+
98
+ result&.round(2)
99
+ rescue => e
100
+ Rails.logger.warn "Error calculating metro area: #{e.message}"
101
+ nil
102
+ end
103
+
104
+ # Get the geographic center (centroid) of this metro
105
+ #
106
+ # Calculates the centroid of all constituent boundaries combined,
107
+ # providing a representative center point for the metropolitan area.
108
+ #
109
+ # @return [Array<Float>, nil] [latitude, longitude] of metro center, or nil if error
110
+ #
111
+ # @example
112
+ # metro.centroid
113
+ # # => [37.4419, -122.1430] (lat, lng of metro center)
114
+ def centroid
115
+ return nil unless geoboundaries.any?
116
+
117
+ result = self.class.connection.select_one(<<~SQL.squish)
118
+ SELECT
119
+ ST_Y(ST_Centroid(ST_Union(boundary))) AS lat,
120
+ ST_X(ST_Centroid(ST_Union(boundary))) AS lng
121
+ FROM geoboundaries
122
+ WHERE id IN (#{geoboundary_ids.join(',')})
123
+ SQL
124
+
125
+ result ? [result['lat'].to_f, result['lng'].to_f] : nil
126
+ rescue => e
127
+ Rails.logger.warn "Error calculating metro centroid: #{e.message}"
128
+ nil
129
+ end
130
+
131
+ # Get all boundary names that define this metro
132
+ #
133
+ # Returns a list of all constituent boundary names for understanding
134
+ # the geographic composition of this metropolitan area.
135
+ #
136
+ # @return [Array<String>] Array of boundary names
137
+ #
138
+ # @example
139
+ # metro.boundary_names
140
+ # # => ["San Francisco County", "Alameda County", "Santa Clara County", ...]
141
+ def boundary_names
142
+ geoboundaries.pluck(:name).compact.sort
143
+ end
144
+
145
+ # Get administrative levels represented in this metro
146
+ #
147
+ # Returns the different administrative levels (ADM1, ADM2, etc.) that
148
+ # make up this metropolitan area.
149
+ #
150
+ # @return [Array<String>] Array of administrative levels
151
+ #
152
+ # @example
153
+ # metro.admin_levels
154
+ # # => ["ADM1", "ADM2"] (includes state and county level boundaries)
155
+ def admin_levels
156
+ geoboundaries.distinct.pluck(:level).compact.sort
157
+ end
158
+
159
+ # Check if this metro spans multiple states/provinces
160
+ #
161
+ # Determines if the metro includes boundaries from multiple ADM1
162
+ # (state/province) level divisions.
163
+ #
164
+ # @return [Boolean] true if metro spans multiple states/provinces
165
+ #
166
+ # @example
167
+ # metro.multi_state?
168
+ # # => true (if metro crosses state boundaries)
169
+ def multi_state?
170
+ state_boundaries = geoboundaries.where(level: "ADM1")
171
+ state_boundaries.count > 1
172
+ end
173
+
174
+ # Get population density (people per km²)
175
+ #
176
+ # Calculates population density based on total population and area,
177
+ # if both values are available.
178
+ #
179
+ # @return [Float, nil] Population density in people per km², or nil if data unavailable
180
+ #
181
+ # @example
182
+ # metro.population_density
183
+ # # => 1547.3 (people per square kilometer)
184
+ def population_density
185
+ return nil unless population && total_area_km2
186
+ return nil if total_area_km2.zero?
187
+
188
+ (population.to_f / total_area_km2).round(1)
189
+ end
190
+
191
+ # Returns a human-readable description of this metro
192
+ #
193
+ # Combines name, details, country, and constituent boundary information
194
+ # for comprehensive metro identification.
195
+ #
196
+ # @return [String] Formatted description
197
+ #
198
+ # @example
199
+ # metro.display_name
200
+ # # => "San Francisco Bay Area (US) - 9 counties, 2 admin levels"
201
+ def display_name
202
+ parts = [name]
203
+ parts << "(#{country_code})" if country_code.present?
204
+
205
+ if geoboundaries.any?
206
+ boundary_count = geoboundaries.count
207
+ level_count = admin_levels.count
208
+ parts << "#{boundary_count} boundaries, #{level_count} admin level#{'s' if level_count != 1}"
209
+ end
210
+
211
+ description = parts.join(" - ")
212
+ description += "\n#{details}" if details.present?
213
+ description
214
+ end
215
+
216
+ # Ensure country code is stored in uppercase
217
+ def country_code=(value)
218
+ super(value&.upcase)
219
+ end
220
+
221
+ # Get a summary of this metro's geographic composition
222
+ #
223
+ # Returns detailed information about the boundaries and administrative
224
+ # levels that make up this metropolitan area.
225
+ #
226
+ # @return [Hash] Summary with boundary counts by level and names
227
+ #
228
+ # @example
229
+ # metro.geographic_summary
230
+ # # => {
231
+ # # total_boundaries: 9,
232
+ # # by_level: {"ADM1" => 1, "ADM2" => 8},
233
+ # # boundary_names: ["San Francisco County", "Alameda County", ...],
234
+ # # spans_multiple_states: false
235
+ # # }
236
+ def geographic_summary
237
+ {
238
+ total_boundaries: geoboundaries.count,
239
+ by_level: geoboundaries.group(:level).count,
240
+ boundary_names: boundary_names,
241
+ spans_multiple_states: multi_state?,
242
+ admin_levels: admin_levels,
243
+ total_area_km2: total_area_km2,
244
+ population_density: population_density
245
+ }
246
+ end
247
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+
5
+ module HasGeoLookup
6
+ class Railtie < Rails::Railtie
7
+ rake_tasks do
8
+ load File.expand_path('../tasks/has_geo_lookup.rake', __dir__)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HasGeoLookup
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "active_support/concern"
5
+ require "active_record"
6
+
7
+ require_relative "has_geo_lookup/version"
8
+ require_relative "has_geo_lookup/concern"
9
+
10
+ # Require models
11
+ require_relative "has_geo_lookup/models/geoname"
12
+ require_relative "has_geo_lookup/models/geoboundary"
13
+ require_relative "has_geo_lookup/models/feature_code"
14
+ require_relative "has_geo_lookup/models/metro"
15
+
16
+ # Require utilities
17
+ require_relative "has_geo_lookup/index_checker"
18
+
19
+ # Require generators and railtie if Rails is available
20
+ if defined?(Rails)
21
+ require_relative "generators/has_geo_lookup/install_generator"
22
+ require_relative "has_geo_lookup/railtie"
23
+ end
24
+
25
+
26
+ module HasGeoLookup
27
+ class Error < StandardError; end
28
+ end