lib/Geo/Coder/Free/Local.pm — Duplicate Code Regions
◆ Amber lines duplicate code in another file — hover for location.
◆ Purple: duplicate of another region within this same file.
1: package Geo::Coder::Free::Local;
2:
3: use strict;
4: use warnings;
5: use autodie qw(:all);
6:
7: use Carp;
8: use Geo::Location::Point 0.19; # 0.16 stores false values; 0.19 derives an accuracy from the fields matched
9: use Geo::Coder::Free::MissCache;
10: use Geo::Coder::Free::Provenance qw(GEOCODER);
11: use Geo::Coder::Free::Utils qw(_abbreviate _normalize);
12: use Geo::StreetAddress::US;
13: use Lingua::EN::AddressParse;
14: use Locale::CA;
15: use Locale::US;
16: use Object::Configure;
17: use Params::Get;
18: use Readonly;
19: use Text::xSV::Slurp;
20: # Imported functions are not methods: without this, $obj->croak() and the like
21: # would resolve.
22: use namespace::clean;
23:
24: =encoding utf-8
25:
26: =head1 NAME
27:
28: Geo::Coder::Free::Local - Geocode using user-curated local data
29:
30: =head1 VERSION
31:
32: Version 0.51
33:
34: =cut
35:
36: our $VERSION = '0.51';
37:
38: =head1 SYNOPSIS
39:
40: use Geo::Coder::Free::Local;
41:
42: my $geocoder = Geo::Coder::Free::Local->new();
43: my $location = $geocoder->geocode(location => 'Ramsgate, Kent, UK');
44: printf "lat=%.6f lon=%.6f\n", $location->lat(), $location->long();
45:
46: =head1 DESCRIPTION
47:
48: Provides geocoding via a user-curated CSV dataset embedded in the module's
49: C<__DATA__> section. Locations in the data were verified by GPS and by
50: inspecting geotagged photographs. The data is read once at construction time
51: and indexed for fast lookup.
52:
53: This is the highest-priority backend tried by C<Geo::Coder::Free>.
54:
55: =head1 LIMITATIONS
56:
57: =over 4
58:
59: =item * The embedded C<__DATA__> dataset covers only a small set of hand-picked locations.
60: There is no mechanism for non-authors to contribute data without patching the module.
61:
62: =item * Canadian and Australian address parsing is not yet implemented; those queries return C<undef>.
63:
64: =item * C<_search> performs an O(n) linear scan through all rows. The hash-based
65: index in C<new()> handles exact string matches; no index exists for partial field matches.
66:
67: =item * C<our %alternatives> duplicates mappings in C<Geo::Coder::Free::__DATA__>.
68: Both should be consolidated into a shared external config file.
69:
70: =item * C<$libpostal_is_installed> is a module-level (effectively global) flag.
71: Not thread-safe in a forking or threaded Perl deployment.
72:
73: =item * The C<__DATA__> filehandle is a one-shot resource. The first call to
74: C<new()> exhausts it with C<my @data = E<lt>DATAE<gt>>; every subsequent
75: C<new()> in the same process reads zero rows and builds an empty index.
76: Construct exactly one C<Local> object per process and share it.
77:
78: =item * The hash-index key is C<lc(Geo::Location::Point-E<gt>new($row)-E<gt>as_string())>,
79: which B<includes the C<name> field>. To get a direct index hit the caller must
80: supply the full string with the venue/place name as the leading component;
81: an address that omits the name falls through to the slower O(n) C<_search> path.
82:
83: =back
84:
85: =cut
86:
87: # Libpostal detection states - avoid magic numbers
88: use constant LIBPOSTAL_UNKNOWN => 0;
89: use constant LIBPOSTAL_INSTALLED => 1;
90: use constant LIBPOSTAL_NOT_INSTALLED => -1;
91:
92: # Module-level singleton flags - see LIMITATIONS re: thread safety.
93: our $libpostal_is_installed = LIBPOSTAL_UNKNOWN;
94:
95: # Lazily initialised Locale singletons to avoid repeated object construction
96: # (Locale::US->new() and Locale::CA->new() are called multiple times per geocode;
97: # caching them reduces object churn on busy lookups).
98: my $_locale_us;
99: my $_locale_ca;
100:
101: # Confidence thresholds for _search results, aligned with OpenAddresses backend
102: Readonly::Scalar my $CONF_EXACT => 1.0;
103: Readonly::Scalar my $CONF_HIGH => 0.7;
104: Readonly::Scalar my $CONF_MEDIUM => 0.5;
105:
106: # Hard-coded place-name aliases for ambiguous or database-inconsistent locations.
107: # MAINTENANCE NOTE: the same mappings appear in Geo::Coder::Free::__DATA__.
108: # Both should eventually be replaced by a shared external config file.
109: our %alternatives = (
110: 'ST LAWRENCE, THANET, KENT' => 'RAMSGATE, KENT',
111: 'ST PETERS, THANET, KENT' => 'ST PETERS, KENT',
112: 'MINSTER, THANET, KENT' => 'RAMSGATE, KENT',
113: 'TYNE AND WEAR' => 'BOROUGH OF NORTH TYNESIDE',
114: );
115:
116: =head1 METHODS
117:
118: =head2 new
119:
120: =head3 SYNOPSIS
121:
122: my $geocoder = Geo::Coder::Free::Local->new();
123: my $geocoder = Geo::Coder::Free::Local->new(cache => $chi_cache);
124:
125: =head3 DESCRIPTION
126:
127: Constructor. Reads the C<__DATA__> CSV block, builds a hash-based lookup
128: index, and derives the geographic centre of any city/state/country cluster
129: containing three or more data points.
130:
131: B<One-shot filehandle>: the C<DATA> handle is consumed on the first call to
132: C<new()>. Any subsequent call to C<new()> returns an object whose dataset and
133: index are both empty. In code that constructs multiple C<Local> objects (e.g.
134: in tests) construct B<exactly one> instance and reuse it across all callers.
135:
136: =head3 API SPECIFICATION
137:
138: =head4 input
139:
140: # Input schema (Params::Validate::Strict)
141: cache => { type => 'object', optional => 1, can => ['get', 'set'] } # CHI-compatible cache object
142:
143: =head4 output
144:
145: # Output schema (Return::Set)
146: { type => 'object', isa => 'Geo::Coder::Free::Local' }
147:
148: =head3 FORMAL SPECIFICATION
149:
150: LocalState ::= ⟨⟨ data : seq Row;
151: index : Map[STRING → Row];
152: cache : Map[STRING → Point] ⟩⟩
153:
154: Init : Params → LocalState
155: ∀ p : Params •
156: let rows == parse_csv(__DATA__) ∪ geographic_centres(__DATA__) •
157: let idx_key == λ r • lc(Point(r).as_string()) •
158: LocalState.index = { idx_key(r) ↦ r | r ∈ rows }
159:
160: =cut
161:
162: sub new {
163: my $class = shift;
164: my $params = Params::Get::get_params(undef, @_) // {};
165:
166: if (!defined($class)) {
167: $class = __PACKAGE__; # FIXME: only works when no arguments are given
168: } elsif (ref($class)) {
169: return bless { %{$class}, %{$params} }, ref($class);
170: }
171:
172: $params = Object::Configure::configure($class, $params);
173:
174: my @data = <DATA>;
175:
176: my $self = bless {
177: data => xsv_slurp(
178: shape => 'aoh',
179: text_csv => {
180: allow_loose_quotes => 1,
181: blank_is_undef => 1,
182: empty_is_undef => 1,
183: binary => 1,
184: escape_char => '\\',
185: },
186: string => \join('', grep { !/^\s*(#|$)/ } @data),
187: ),
188: %{$params},
189: }, $class;
190:
191: # Derive geographic centres from clusters of 3+ co-located data points,
192: # adding implicit town/city records to the dataset.
193: my $towns = _find_geographic_centres($self->{'data'});
194: push @{$self->{'data'}}, @{$towns} if $towns;
195:
196: # Build a hash index on the stringified Geo::Location::Point representation
197: # so that exact-match lookups avoid the O(n) scan in _search.
198: for my $row (@{$self->{'data'}}) {
199: my $key = lc(Geo::Location::Point->new($row)->as_string());
200: $self->{'index'}{$key} = $row;
201: }
202:
203: # Build an O(1) region existence index used by geocode() to skip the O(n)
204: # grep before _search when no rows for the requested state/country exist.
205: # Major: every (state|country) pair in the dataset is represented here.
206: # Minor: if geocode's target pair is absent, _search can never match.
207: # Conclusion: early return is safe - eliminates the O(n) grep per call.
208: for my $row (@{$self->{'data'}}) {
209: my $rk = uc($row->{'state'} // '') . '|' . uc($row->{'country'} // '');
210: $self->{'region_index'}{$rk} = 1;
211: }
212:
213: return $self;
214: }
215:
216: # What this backend counts as a miss, and where the whole process remembers
217: # it. The data is in __DATA__, so every instance has the same data and a miss
218: # found by one is a miss for all of them.
219: sub _miss_scope {
220: return Geo::Coder::Free::MissCache->scope_for('Local');
221: }
222:
223: sub _missed {
224: my ($self, $lc) = @_;
225:
226: return Geo::Coder::Free::MissCache->missed($self->_miss_scope(), $lc);
227: }
228:
229: sub _remember_miss {
230: my ($self, $lc) = @_;
231:
232: Geo::Coder::Free::MissCache->remember($self->_miss_scope(), $lc);
233: return;
234: }
235:
236: =head2 reads
237:
238: my $count = $geocoder->reads();
239:
240: How many times this backend has searched its dataset since the object was
241: created. A lookup answered from the index, from the result cache or from the
242: shared miss cache does not increase it, so C<geocode_many()> can take the count
243: before and after a value to tell a lookup that cost a search from one that was
244: answered from a cache.
245:
246: =head3 API SPECIFICATION
247:
248: =head4 input
249:
250: # Input schema (Params::Validate::Strict)
251: # none
252:
253: =head4 output
254:
255: # Output schema (Return::Set)
256: { type => 'integer', min => 0 } # 0 for a new object
257:
258: =cut
259:
260: sub reads {
261: my $self = shift;
262:
263: return $self->{'reads'} // 0;
264: }
265:
266: =head2 geocode
267:
268: =head3 SYNOPSIS
269:
270: my $pt = $geocoder->geocode(location => '203 E Chatsworth Rd, Reisterstown, Baltimore, MD, US');
271: print $pt->lat(), "\n";
272:
273: # All calling forms are accepted:
274: $geocoder->geocode('203 E Chatsworth Rd, Reisterstown, MD, US');
275: $geocoder->geocode({ location => '203 E Chatsworth Rd, Reisterstown, MD, US' });
276:
277: =head3 API SPECIFICATION
278:
279: =head4 input
280:
281: # Input schema (Params::Validate::Strict)
282: location => { type => 'scalar' } # address string; must contain at least two commas
283:
284: =head4 output
285:
286: # Output schema (Return::Set)
287: { type => 'object', isa => 'Geo::Location::Point', optional => 1 }
288:
289: =head3 PROVENANCE
290:
291: Every result carries C<geocoder>, C<database>, C<confidence> and C<accuracy>
292: (section 2.1 of F<BI-Phase6-Dependency-Specs.md>). This backend reports
293: C<database> as C<Geo::Coder::Free::Local>, and C<accuracy> is what the matched
294: columns justify: a house number makes it an C<address>, a road a C<street>, a
295: name or city a C<locality>, a state a C<region> and a country a C<country>. A
296: lookup answered from the index, which matches a whole row, is an exact match
297: (C<confidence> of 1.0) whose accuracy comes from the row's own fields - so a
298: derived geographic centre, which carries a city, state and country and nothing
299: narrower, is a C<locality>.
300:
301: =head3 MESSAGES
302:
303: Usage: ...::geocode(...) No location argument given.
304:
305: =head3 FORMAL SPECIFICATION
306:
307: Geocode : STRING → Point?
308: ∀ addr : STRING •
309: let norm == lc(replace_usa(addr)) •
310: (norm ∈ cache ⟹ result = cache[norm]) ∧
311: (norm ∈ index ⟹ result = index[norm]) ∧
312: (¬ result ⟹ result = parse_and_search(addr))
313:
314: =head3 PSEUDOCODE
315:
316: normalise @_ into %params
317: reject if location does not contain two or more commas (not a full address)
318: check cache; return hit
319: check hash index; return hit and cache it
320: # index key includes the 'name' field; supply the venue/place name as the
321: # leading address component to guarantee an index hit
322: attempt country-specific parser (US, GB; skip CA/AU pending implementation)
323: attempt Geo::StreetAddress::US for "..., USA" addresses
324: attempt Geo::Address::Parser
325: attempt Geo::libpostal (large memory footprint; loaded lazily)
326: attempt 4-part regex decomposition (name/road/city/state/country)
327: attempt %alternatives mapping
328: return undef
329:
330: =cut
331:
332: sub geocode {
333: my $self = shift;
334:
335: # Handle being called as a function rather than a method
336: if (!ref($self)) {
337: if (scalar @_) {
338: return __PACKAGE__->new()->geocode(@_);
339: } elsif (!defined($self)) {
340: Carp::croak('Usage: ', __PACKAGE__, '::geocode(location => $location)');
341: } elsif ($self eq __PACKAGE__) {
342: Carp::croak("Usage: $self", '::geocode(location => $location)');
343: }
344: return __PACKAGE__->new()->geocode($self);
345: } elsif (ref($self) eq 'HASH') {
346: return __PACKAGE__->new()->geocode($self);
347: }
348:
349: my %params = _normalize_args(
350: 'Usage: ' . __PACKAGE__ . '::geocode(location => $location)',
351: 'location', @_
352: );
353:
354: my $location = $params{'location'}
355: or Carp::croak('Usage: geocode(location => $location)');
356:
357: # Normalise the lookup key - "USA" is treated the same as "US"
358: my $lc = lc($location);
359: $lc =~ s/,\s*usa$/, us/i;
360:
361: return $self->{'cache'}{$lc} if exists $self->{'cache'}{$lc};
362: return $self->_cache_and_return($lc, $self->_result_for($self->{'index'}{$lc}))
363: if exists $self->{'index'}{$lc};
364:
365: # A miss found by an earlier instance, or an earlier call through this
366: # one, means the same search would find nothing again: the data does not
367: # change while the process runs. clear_miss_cache() is for when it has.
368: return if $self->_missed($lc);
369:
370: # This backend only handles full addresses (at least "road, city, country")
371: return $self->_remember_miss($lc) if $location !~ /,.+,/;
372:
373: my $ap;
374: if ($location =~ /USA?$/ || $location =~ /United States$/) {
375: $ap = $self->{'ap'}{'us'} //= Lingua::EN::AddressParse->new(
376: country => 'US', auto_clean => 1, force_case => 1, force_post_code => 0
377: );
378: } elsif ($location =~ /(England|Scotland|Wales|Northern Ireland|UK|GB)$/i) {
379: $ap = $self->{'ap'}{'gb'} //= Lingua::EN::AddressParse->new(
380: country => 'GB', auto_clean => 1, force_case => 1, force_post_code => 0
381: );
382: } elsif ($location =~ /Canada$/) {
383: # TODO: Canadian address parsing not yet implemented
384: return $self->_remember_miss($lc);
385: } elsif ($location =~ /Australia$/) {
386: # TODO: Australian address parsing not yet implemented
387: return $self->_remember_miss($lc);
388: }
389:
390: if ($ap) {
391: my $l = $location;
392: $l =~ s/(.+), (England|UK)$/$1, GB/i;
393:
394: if ($ap->parse($l) == 0) {
395: my %c = $ap->components();
396: my %addr = (location => $l);
397:
398: if (my $street = $c{'street_name'}) {
399: if (my $type = $c{'street_type'}) {
400: my $abbrev = _abbreviate($type);
401: $street .= ' ' . ($abbrev || $type);
402: $street .= ' ' . $c{'street_direction_suffix'}
403: if $c{'street_direction_suffix'};
404: $street =~ s/^0+//;
405: $addr{'road'} = $street;
406: }
407: }
408:
409: if (length($c{'subcountry'}) == 2) {
410: $addr{'state'} = $c{'subcountry'};
411: } else {
412: my $country_raw = $c{'country'} // '';
413: if ($country_raw =~ /Canada/i) {
414: $addr{'country'} = 'CA';
415: $addr{'state'} = _to_two_letter_state('Canada', $c{'subcountry'});
416: } elsif ($country_raw =~ /^(United States|USA|US)$/i) {
417: $addr{'country'} = 'US';
418: $addr{'state'} = _to_two_letter_state('US', $c{'subcountry'});
419: } elsif ($country_raw) {
420: $addr{'country'} = $country_raw;
421: $addr{'state'} = $c{'subcountry'} if $c{'subcountry'};
422: }
423: }
424: $addr{'number'} = $c{'property_identifier'};
425: $addr{'city'} = $c{'suburb'};
426:
427: if (my $rc = $self->_search(\%addr, qw(number road city state country))) {
428: return $self->_cache_and_return($lc, $rc);
429: }
430: if ($addr{'number'}) {
431: if (my $rc = $self->_search(\%addr, qw(road city state country))) {
432: return $self->_cache_and_return($lc, $rc);
433: }
434: }
435:
436: # Abort early if no data at all matches this state/country -
437: # saves traversing the full dataset for every remaining strategy.
438: if (!defined($addr{'country'})) {
439: $addr{'country'} = $l =~ /(United States|USA|US)$/i ? 'US'
440: : Carp::croak("TODO: extract country from $l");
441: }
442: # O(1) region check via index built in new() - replaces the former
443: # O(n) grep across all rows.
444: # Major: region_index maps every (state|country) pair in the dataset.
445: # Minor: if target pair is absent, no _search call can match.
446: # Conclusion: early return is equivalent and faster.
447: my $rk = uc($addr{'state'} // '') . '|' . uc($addr{'country'} // '');
448: return $self->_remember_miss($lc) unless $self->{'region_index'}{$rk};
449: }
450: }
451:
452: if ($location =~ /^(.+?)[,\s]+(United States|USA|US)$/i) {
453: my $l = $1;
454: $l =~ tr/,/ /;
455: $l =~ s/\s{2,}/ /g;
456:
457: # Geo::StreetAddress::US is buggy (RT#122617) - skip county-style addresses
458: if ($location !~ /\sCounty,/i) {
459: my $href = Geo::StreetAddress::US->parse_location($l)
460: // Geo::StreetAddress::US->parse_address($l);
461:
462: if ($href) {
463: if (my $state = $href->{'state'}) {
464: $state = _to_two_letter_state('US', $state) if length($state) > 2;
465: my $city = uc($href->{'city'} // '');
466: if (my $street = $href->{'street'}) {
467: if ($href->{'type'}) {
468: $street .= ' ' . _abbreviate($href->{'type'});
469: }
470: $street .= ' ' . $href->{'suffix'} if $href->{'suffix'};
471: $street = $href->{'prefix'} . " $street" if $href->{'prefix'};
472: my %addr = (
473: number => $href->{'number'},
474: road => $street,
475: city => $city,
476: state => $state,
477: country => 'US',
478: );
479: if ($href->{'number'}) {
480: if (my $rc = $self->_search(\%addr, qw(number road city state country))) {
481: $rc->{'country'} = 'US';
482: return $self->_cache_and_return($lc, $rc);
483: }
484: }
485: if (my $rc = $self->_search(\%addr, qw(road city state country))) {
486: $rc->{'country'} = 'US';
487: return $self->_cache_and_return($lc, $rc);
488: }
489: # G:S:US puts building name into street when number is absent
490: if ($street && !$href->{'number'}) {
491: $addr{'name'} = delete $addr{'road'};
492: if (my $rc = $self->_search(\%addr, qw(name city state country))) {
493: $rc->{'country'} = 'US';
494: return $self->_cache_and_return($lc, $rc);
495: }
496: }
497: }
498: }
499: }
500: }
501:
502: # Fallback: try "name, street, town, state, US" split
503: my @parts = split(/,\s*/, $location);
504: if (scalar(@parts) == 5) {
505: my $state = _to_two_letter_state('US', $parts[3]);
506: if (length($state) == 2) {
507: my %addr = (city => $parts[2], state => $state, country => 'US');
508: if ($parts[0] !~ /^\d/) {
509: $addr{'name'} = $parts[0];
510: if ($parts[1] =~ /^(\d+)\s+(.+)/) {
511: $addr{'number'} = $1;
512: $addr{'road'} = _normalize($2);
513: if (my $rc = $self->_search(\%addr, qw(name number road city state country))) {
514: $rc->{'country'} = 'US';
515: return $self->_cache_and_return($lc, $rc);
516: }
517: } else {
518: $addr{'road'} = _normalize($parts[1]);
519: if (my $rc = $self->_search(\%addr, qw(name road city state country))) {
520: $rc->{'country'} = 'US';
521: return $self->_cache_and_return($lc, $rc);
522: }
523: }
524: } else {
525: $addr{'number'} = $parts[0];
526: $addr{'road'} = _normalize($parts[1]);
527: if (my $rc = $self->_search(\%addr, qw(number road city state country))) {
528: $rc->{'country'} = 'US';
529: return $self->_cache_and_return($lc, $rc);
530: }
531: }
532: }
533: }
534: }
535:
536: # Simple "Town, County, England" - no further sub-parsers will help
537: if (($location =~ /[^,]+,[^,]+,.*England$/) && ($location !~ /[^,]+,[^,]+,[^,]+,.*England$/)) {
538: return $self->_remember_miss($lc);
539: }
540:
541: # Geo::Address::Parser - optional; load lazily, skip if not installed
542: unless (Geo::Address::Parser->can('parse')) {
543: eval { require Geo::Address::Parser; Geo::Address::Parser->import() };
544: }
545: if (Geo::Address::Parser->can('parse')) {
546: my $addr_parser = Geo::Address::Parser->new(country => 'UK');
547: if (my $fields = $addr_parser->parse($location)) {
548: # Remove undef fields so _search only matches defined columns
549: delete $fields->{$_} for grep { !defined $fields->{$_} } keys %{$fields};
550: if (my $rc = $self->_search($fields, keys %{$fields})) {
551: $rc->{'country'} = 'UK';
552: return $self->_cache_and_return($lc, $rc);
553: }
554: }
555: }
556:
557: # Geo::libpostal - accurate but uses enormous RAM; initialise at most once
558: if ($libpostal_is_installed == LIBPOSTAL_UNKNOWN) {
559: if (eval { require Geo::libpostal; 1 }) {
560: Geo::libpostal->import();
561: $libpostal_is_installed = LIBPOSTAL_INSTALLED;
562: } else {
563: $libpostal_is_installed = LIBPOSTAL_NOT_INSTALLED;
564: }
565: }
566:
567: if ($libpostal_is_installed == LIBPOSTAL_INSTALLED) {
568: my %addr = Geo::libpostal::parse_address($location);
569: if (%addr) {
570: # Normalise field names to match our data schema
571: $addr{'number'} = delete $addr{'house_number'} if $addr{'house_number'} && !$addr{'number'};
572: $addr{'name'} = delete $addr{'house'} if $addr{'house'} && !$addr{'name'};
573: $addr{'location'} = $location;
574:
575: if (my $street = $addr{'road'}) {
576: $addr{'road'} = _normalize($street);
577: }
578:
579: # libpostal returns "england" as a state - map to country code
580: if (defined($addr{'state'}) && !defined($addr{'country'})
581: && $addr{'state'} eq 'england') {
582: delete $addr{'state'};
583: $addr{'country'} = 'GB';
584: }
585:
586: if ($addr{'country'} && ($addr{'state'} || $addr{'state_district'})) {
587: if ($addr{'country'} =~ /Canada/i) {
588: $addr{'country'} = 'Canada';
589: $addr{'state'} = _to_two_letter_state('Canada', $addr{'state'})
590: if $addr{'state'} && length($addr{'state'}) > 2;
591: } elsif ($addr{'country'} =~ /^(United States|USA|US)$/i) {
592: $addr{'country'} = 'US';
593: $addr{'state'} = _to_two_letter_state('US', $addr{'state'})
594: if $addr{'state'} && length($addr{'state'}) > 2;
595: }
596:
597: if ($addr{'state_district'}) {
598: $addr{'state_district'} =~ s/^(.+)\s+COUNTY\s*$/$1/i;
599: if (my $rc = $self->_search(\%addr, qw(number road city state_district state country))) {
600: return $self->_cache_and_return($lc, $rc);
601: }
602: }
603: if (my $rc = $self->_search(\%addr, qw(number road city state country))) {
604: return $self->_cache_and_return($lc, $rc);
605: }
606: if ($addr{'number'}) {
607: if (my $rc = $self->_search(\%addr, qw(road city state country))) {
608: return $self->_cache_and_return($lc, $rc);
609: }
610: }
611: }
612: }
613: }
614:
615: # Last resort: decompose "road, city, state, country" with a regex
616: if ($location =~ /^(.+?),\s*([\s\w]+),\s*([\s\w]+),\s*([\w\s]+)$/) {
617: my %addr = (
618: road => $1,
619: city => $2,
620: state => $3,
621: country => $4,
622: );
623: $addr{'state'} =~ s/\s+$//g;
624: $addr{'country'} =~ s/\s+$//g;
625:
626: if ($addr{'road'} =~ /([\w\s]+),*\s+(.+)/) {
627: $addr{'name'} = $1;
628: $addr{'road'} = $2;
629: }
630: if ($addr{'road'} =~ /^(\d+)\s+(.+)/) {
631: $addr{'number'} = $1;
632: $addr{'road'} = $2;
633: if (my $rc = $self->_search(\%addr, qw(name number road city state country))) {
634: return $self->_cache_and_return($lc, $rc);
635: }
636: } elsif (my $rc = $self->_search(\%addr, qw(name road city state country))) {
637: return $self->_cache_and_return($lc, $rc);
638: }
639: if ($addr{'name'} && !defined($addr{'number'})) {
640: if (my $rc = $self->_search(\%addr, qw(name road city state country))) {
641: return $self->_cache_and_return($lc, $rc);
642: }
643: }
644: }
645:
646: # Try the alternatives table - curated mappings for inconsistent names
647: $location = uc($location);
648: for my $left (keys %alternatives) {
649: next unless $location =~ $left;
650: (my $mapped = $location) =~ s/$left/$alternatives{$left}/;
651: $params{'location'} = $mapped;
652: if (my $rc = $self->geocode(\%params)) {
653: return $self->_cache_and_return($lc, $rc);
654: }
655: if ($mapped =~ /(.+), (England|UK)$/i) {
656: $params{'location'} = "$1, GB";
657: if (my $rc = $self->geocode(\%params)) {
658: return $self->_cache_and_return($lc, $rc);
659: }
660: }
661: $params{'location'} = $location;
662: }
663:
664: return $self->_remember_miss($lc);
665: }
666:
667: =head2 reverse_geocode
668:
669: =head3 SYNOPSIS
670:
671: my $loc = $geocoder->reverse_geocode(latlng => '51.3341,-1.4159');
672: # Returns the location string(s) for that lat/lon pair.
673:
674: =head3 API SPECIFICATION
675:
676: =head4 input
677:
678: # Input schema (Params::Validate::Strict) - latlng or lat+lon required
679: latlng => { type => 'scalar', optional => 1 } # "$lat,$long" comma-separated decimal degrees
680: lat => { type => 'scalar', optional => 1 } # latitude (alternative to latlng)
681: lon => { type => 'scalar', optional => 1 } # longitude (alternative to latlng)
682: long => { type => 'scalar', optional => 1 } # alias for lon
683:
684: =head4 output
685:
686: # Output schema (Return::Set)
687: # scalar context: { type => 'scalar', optional => 1 } # location string
688: # list context: { type => 'arrayref', of => { type => 'scalar' } }
689:
690: =cut
691:
692: sub reverse_geocode {
693: my $self = shift;
694:
695: if (!ref($self)) {
696: if (scalar @_) {
697: return __PACKAGE__->new()->reverse_geocode(@_);
698: } elsif (!defined($self)) {
699: Carp::croak('Usage: ', __PACKAGE__, '::reverse_geocode(latlng => "$lat,$long")');
700: } elsif ($self eq __PACKAGE__) {
701: Carp::croak("Usage: $self", '::reverse_geocode(latlng => "$lat,$long")');
702: }
703: return __PACKAGE__->new()->reverse_geocode($self);
704: } elsif (ref($self) eq 'HASH') {
705: return __PACKAGE__->new()->reverse_geocode($self);
706: }
707:
708: my %params = _normalize_args(
709: 'Usage: ' . __PACKAGE__ . '::reverse_geocode(latlng => "$lat,$long")',
710: 'latlng', @_
711: );
712:
713: my ($latitude, $longitude);
714: if (my $latlng = $params{'latlng'}) {
715: ($latitude, $longitude) = split /,/, $latlng;
716: } else {
717: $latitude = $params{'lat'};
718: $longitude = $params{'lon'} // $params{'long'};
719: }
720:
721: if (!defined($latitude) || !defined($longitude)) {
722: Carp::croak('Usage: ', __PACKAGE__, '::reverse_geocode(latlng => "$lat,$long")');
723: }
724:
725: my @rc;
726: for my $row (@{$self->{'data'}}) {
727: next unless defined($row->{'latitude'}) && defined($row->{'longitude'});
728: next unless _equal($row->{'latitude'}, $latitude, 4)
729: && _equal($row->{'longitude'}, $longitude, 4);
730:
731: # Rows in $self->{'data'} are blessed as Geo::Location::Point by
732: # the index-building loop in new() - calling as_string() is safe here.
733: my $location = uc($row->as_string());
734: if (wantarray) {
735: push @rc, $location;
736: # Add any reverse-alternative mappings for this location
737: # keys, not each(): %alternatives is package-wide, and an each()
738: # iterator left part-way would make the next pass skip entries
739: foreach my $left (sort keys %alternatives) {
740: my $right = $alternatives{$left};
741: if ($location =~ $right) {
742: (my $l = $location) =~ s/$right/$left/;
743: push @rc, $l;
744: }
745: }
746: } else {
747: return $location;
748: }
749: }
750: return @rc;
751: }
752:
753: =head2 ua
754:
755: Does nothing - present for drop-in compatibility with other Geo::Coder::* modules.
756:
757: =head3 API SPECIFICATION
758:
759: =head4 input
760:
761: # Input schema (Params::Validate::Strict)
762: # any arguments are accepted and ignored
763:
764: =head4 output
765:
766: # Output schema (Return::Set)
767: # nothing: an empty list, or undef in scalar context
768:
769: =cut
770:
771: ◆sub ua { } 772: ◆ 773: ◆# ----------------------------------------------------------------------- 774: ◆# Private helpers 775: ◆# ----------------------------------------------------------------------- 776: ◆ 777: ◆# Purpose: Normalise the four calling conventions used by geocode/reverse_geocode: 778: ◆# hashref, even-length key-val list, single bare string. 779: ◆# Entry: $error_msg - croak message if an unsupported ref type is passed. 780: ◆# $bare_key - hash key to use when a single bare string is given 781: # ('location' for geocode, 'latlng' for reverse_geocode).
782: # @args - raw @_ after $self has been shifted.
783: # Exit: Flat %params hash.
784: # Side Effects: None; croaks on non-hash reference arguments.
785: sub _normalize_args {
786: my ($error_msg, $bare_key, @args) = @_;
787: return %{$args[0]} if ref($args[0]) eq 'HASH';
788: Carp::croak($error_msg) if ref($args[0]);
789: return @args if @args && @args % 2 == 0;
790: return ($bare_key => $args[0]) if @args == 1;
791: # M7 (logic-gap closure): odd-count > 1 is not a valid calling convention.
792: # Fail fast rather than silently discarding trailing arguments.
793: Carp::croak($error_msg) if @args;
794: return ();
795: }
796:
797: # Purpose: Store $rc in the lookup cache under $key and return it.
798: # Reduces the repeated "$self->{cache}{$k} = $r; return $r;" pattern
799: # that appeared ~12 times across geocode.
800: # Entry: $self, $key (lc string), $rc (Geo::Location::Point or undef).
801: # Exit: $rc (returned transparently).
802: # Side Effects: Writes to $self->{cache}.
803: sub _cache_and_return {
804: my ($self, $key, $rc) = @_;
805: $self->{'cache'}{$key} = $rc;
806: return $rc;
807: }
808:
809: # Purpose: Resolve a full state/province name to its two-letter code using
810: # the appropriate Locale:: module. Returns the original string if
811: # no mapping is found or the input is already two characters.
812: # Locale::US and Locale::CA objects are cached as module-level
813: # singletons to avoid repeated construction overhead.
814: # Entry: $country - country name or ISO code; $state - state/province name.
815: # Exit: Two-letter code or $state unchanged.
816: # Side Effects: May initialise $_locale_us or $_locale_ca singletons.
817: sub _to_two_letter_state {
818: my ($country, $state) = @_;
819: return $state // '' if !defined($state) || length($state) <= 2;
820:
821: if ($country =~ /^(United States|USA|US)$/i) {
822: $_locale_us //= Locale::US->new();
823: return $_locale_us->{'state2code'}{uc($state)} // $state;
824: } elsif ($country =~ /Canada/i) {
825: $_locale_ca //= Locale::CA->new();
826: return $_locale_ca->{'province2code'}{uc($state)} // $state;
827: }
828: return $state;
829: }
830:
831: # Purpose: Match parsed address components against all data rows.
832: # Each named column must match (case-insensitively) the corresponding
833: # data field; a minimum of 3 columns must match for a result.
834: # Entry: $self, $data - hashref of address components,
835: # @columns - ordered list of keys to compare.
836: # Exit: Geo::Location::Point on match; undef otherwise.
837: # Side Effects: May delete undef entries from $data (for undefined optional fields).
838: sub _search {
839: my ($self, $data, @columns) = @_;
840:
841: $self->{'reads'}++;
842:
843: # M5 (boolean reduction): the $failed flag was set-and-break in the inner loop
844: # then tested in the outer loop - eliminated by using a labeled 'next ROW'
845: # that short-circuits directly, removing one boolean gate per iteration.
846: ROW: for my $row (@{$self->{'data'}}) {
847: my $matched = 0;
848: my @matched_columns;
849:
850: for my $column (@columns) {
851: if (defined($data->{$column})) {
852: next ROW unless defined($row->{$column});
853: next ROW if uc($row->{$column}) ne uc($data->{$column});
854: $matched++;
855: # The accuracy is reported from the columns that matched, not
856: # every column this call site offered: a caller trying
857: # 'number road city...' for a town that has no road must not have
858: # its match described as an address.
859: push @matched_columns, $column;
860: } elsif (exists $data->{$column}) {
861: delete $data->{$column};
862: }
863: }
864:
865: next if $matched < 3;
866:
867: # Assign confidence based on how many columns contributed to the match
868: my $confidence = $matched == scalar(@columns) ? $CONF_EXACT
869: : $matched >= 4 ? $CONF_HIGH
870: : $CONF_MEDIUM;
871:
872: # Provenance (Phase 6 dependency specs section 2.1). Set after %{$row}
873: # so that a dataset column that happens to be called accuracy cannot stand
874: # in for the match this code actually made.
875: return Geo::Location::Point->new(
876: location => $data->{'location'},
877: confidence => $confidence,
878: database => __PACKAGE__,
879: %{$row},
880: geocoder => GEOCODER,
881: accuracy => Geo::Location::Point::accuracy_for(@matched_columns),
882: );
883: }
884: return;
885: }
886:
887: =head2 _result_for
888:
889: Build the result for a dataset row that the index matched. The index holds the
890: rows themselves - the same hashrefs C<_search> scans - so the object is built
891: as a copy rather than blessed in place, and the provenance keys every result
892: carries (section 2.1 of F<BI-Phase6-Dependency-Specs.md>) are added where the
893: row does not already record them.
894:
895: An index hit is an exact match on the row's string, so the confidence is
896: C<$CONF_EXACT>, and the accuracy is what the row's own fields justify, through
897: C<Geo::Location::Point::accuracy_for()>.
898:
899: =cut
900:
901: sub _result_for {
902: my ($self, $row) = @_;
903:
904: my %attributes = %{$row};
905: $attributes{'geocoder'} = GEOCODER;
906: $attributes{'database'} = __PACKAGE__;
907: $attributes{'confidence'} = $CONF_EXACT;
908: $attributes{'accuracy'} = Geo::Location::Point::accuracy_for(keys %attributes);
909:
910: return Geo::Location::Point->new(\%attributes);
911: }
912:
913: # Purpose: Calculate the geographic centres (centroids) of all city/state/country
914: # clusters in the parsed dataset that contain three or more data points.
915: # Clusters with fewer than 3 entries are too sparse to be meaningful.
916: # Entry: $rows - arrayref of hashrefs with latitude/longitude/city/state/country.
917: # Exit: Arrayref of hashrefs (one per qualifying cluster), or undef if none.
918: # Side Effects: None.
919: sub _find_geographic_centres {
920: my $rows = $_[0];
921:
922: # Group rows by normalised city|state|country key
923: my %groups;
924: for my $row (@{$rows}) {
925: next unless defined($row->{'latitude'}) && defined($row->{'longitude'});
926: next unless $row->{'latitude'} =~ /^-?\d+\.?\d*$/;
927: next unless $row->{'longitude'} =~ /^-?\d+\.?\d*$/;
928: my $key = join '|',
929: $row->{'city'} // '',
930: $row->{'state'} // '',
931: $row->{'country'} // '';
932: push @{$groups{$key}}, $row;
933: }
934:
935: my @centres;
936: for my $key (keys %groups) {
937: my $locs = $groups{$key};
938: next if @{$locs} < 3;
939:
940: my ($city, $state, $country) = split /\|/, $key;
941: my ($lat, $lon) = _calculate_centre($locs);
942:
943: push @centres, {
944: city => $city,
945: state => $state,
946: country => $country,
947: lat => $lat,
948: latitude => $lat,
949: longitude => $lon,
950: long => $lon,
951: lng => $lon,
952: };
953: }
954:
955: return @centres ? \@centres : undef;
956: }
957:
958: # Purpose: Compute the arithmetic mean of latitude and longitude for a group
959: # of locations. Accurate for small geographic areas; for large areas
960: # or locations crossing the antimeridian, a vector-mean is needed.
961: # Entry: $locs - arrayref of hashrefs each with 'latitude' and 'longitude'.
962: # Exit: ($centre_lat, $centre_lon) as decimal degree strings to 6dp.
963: # Side Effects: None.
964: sub _calculate_centre {
965: my $locs = $_[0];
966: my ($sum_lat, $sum_lon) = (0, 0);
967: $sum_lat += $_->{'latitude'}, $sum_lon += $_->{'longitude'} for @{$locs};
968: my $n = scalar @{$locs};
969: return (sprintf('%.6f', $sum_lat / $n), sprintf('%.6f', $sum_lon / $n));
970: }
971:
972: # https://www.oreilly.com/library/view/perl-cookbook/1565922433/ch02s03.html
973: # Equal within $dp decimal places - avoids floating-point equality pitfalls.
974: sub _equal {
975: my ($A, $B, $dp) = @_;
976: return sprintf("%.${dp}g", $A) eq sprintf("%.${dp}g", $B);
977: }
978:
979: =head1 AUTHOR
980:
981: Nigel Horne C<< <njh@nigelhorne.com> >>
982:
983: =head1 BUGS
984:
985: The data are stored in the module source and must be maintained by the author.
986: A future version should load them from an external file to allow community contributions.
987:
988: See also: L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Geo-Coder-Free>
989:
990: =head1 SEE ALSO
991:
992: L<Geo::Coder::Free>, L<Geo::Coder::Free::OpenAddresses>, L<Geo::Coder::Free::MaxMind>
993:
994: =head1 LICENSE AND COPYRIGHT
995:
996: Copyright 2020-2026 Nigel Horne.
997:
998: The program code is released under the following licence: GPL2 for personal use on a single computer.
999: All other users (including Commercial, Charity, Educational, and Government)
1000: must apply in writing for a licence for use from Nigel Horne at C<< <njh at nigelhorne.com> >>.
1001:
1002: =cut
1003:
1004: 1;
1005:
1006: # Use abbreviations in the data: RD not ROAD, ST not STREET, etc.
1007: __DATA__
1008: "name","number","road","city","state_district","state","country","latitude","longitude"
1009: "ST ANDREWS CHURCH",,"CHURCH HILL","EARLS COLNE",,"ESSEX","GB",51.926793,0.70408
1010: "WESTWOOD CROSS",23,"MARGATE RD","BROADSTAIRS",,"KENT","GB",51.358967,1.391367
1011: "RECULVER ABBEY",,"RECULVER","HERNE BAY",,"KENT","GB",51.37875,1.1955
1012: "NEW INN",2,"TOTHILL ST","RAMSGATE",,"KENT","GB",51.334522,1.314417
1013: "HOLIDAY INN EXPRESS",,"TOTHILL ST","RAMSGATE",,"KENT","GB",51.34320725,1.31680853
1014: "",106,"TOTHILL ST","RAMSGATE",,"KENT","GB",51.33995174,1.31570211
1015: "",114,"TOTHILL ST","RAMSGATE",,"KENT","GB",51.34015944,1.31580976
1016: "MINSTER CEMETERY",116,"TOTHILL ST","RAMSGATE",,"KENT","GB",51.34203083,1.31609075
1017: "RAMSGATE STATION",,"STATION APPROACH RD","RAMSGATE","","KENT","GB",51.340826,1.406519
1018: "ST MARY THE VIRGIN CHURCH",,"CHURCH ST","RAMSGATE",,"KENT","GB",51.33090893,1.31559716
1019: "",20,"MELBOURNE AVE","RAMSGATE",,"KENT","GB",51.34772374,1.39532565
1020: "TOBY CARVERY",,"NEW HAINE RD","RAMSGATE",,"KENT","GB",51.357510,1.388894
1021: "",,"WESTCLIFF PROMENADE","RAMSGATE",,"KENT","GB",51.32711,1.406806
1022: "TOWER OF LONDON",35,"TOWER HILL","LONDON",,"LONDON","GB",51.5082675,-0.0754225
1023: "",5350,"CHILLUM PL NE","WASHINGTON",,"DC","US",38.955403,-76.996241
1024: "WALTER E. WASHINGTON CONVENTION CENTER",801,"MT VERNON PL NW","WASHINGTON","","DC","US",38.904022,-77.023113
1025: "",7,"JORDAN MILL COURT","WHITE HALL","BALTIMORE","MD","US",39.6852333333333,-76.6071166666667
1026: "ALL SAINTS EPISCOPAL CHURCH",203,"E CHATSWORTH RD","REISTERSTOWN","BALTIMORE","MD","US",39.467270,-76.823947
1027: "BALLPARK RESTAURANT",3418,"CONOWINGO RD","DUBLIN","HARFORD","MD","US",39.633018,-76.272558
1028: "NCBI",,"MEDLARS DR","BETHESDA","MONTGOMERY","MD","US",38.99516556,-77.09943963
1029: "",,"CENTER DR","BETHESDA","MONTGOMERY","MD","US",38.99698114,-77.10031119
1030: "",,"NORFOLK AVE","BETHESDA","MONTGOMERY","MD","US",38.98939358,-77.09819543
1031: "ROCK BOTTOM RESTAURANT & BREWERY",,"NORFOLK AVE","BETHESDA","MONTGOMERY","MD","US",38.9890861111111,-77.0975722222222
1032: "",3516,"SW MACVICAR AVE","TOPEKA","SHAWNEE","KS","US",39.005175,-95.706681
1033: "THE ATRIUM AT ROCK SPRING PARK",6555,"ROCKLEDGE DR","BETHESDA","MONTGOMERY","MD","US",39.028326,-77.136774
1034: "","","MOUTH OF MONOCACY RD","DICKERSON","MONTGOMERY","MD","US",39.2244603797302,-77.449615439877
1035: "PATAPSCO VALLEY STATE PARK'",8020,"BALTIMORE NATIONAL PK","ELLICOTT CITY","HOWARD","MD","US",39.29491,-76.78051
1036: "",,"ANNANDALE RD","EMMITSBURG","FREDERICK","MD","US",39.683529,-77.349405
1037: "UTICA DISTRICT PARK",,,"FREDERICK","FREDERICK","MD","US",39.5167883333333,-77.4015166666667
1038: "",3923,"SUGARLOAF CT","MONROVIA","FREDERICK","MD","US",39.342986,-77.239770
1039: "ALBERT EINSTEIN HIGH SCHOOL",11135,"NEWPORT MILL RD","KENSINGTON","MONTGOMERY","MD","US",39.03869019,-77.0682871
1040: "",10540,"METROPOLITAN AVE","KENSINGTON","MONTGOMERY","MD","US",39.028404,-77.073227
1041: "POST OFFICE",10325,"KENSINGTON PKWY","KENSINGTON","MONTGOMERY","MD","US",39.02554455,-77.07178215
1042: "NEWPORT MILL MIDDLE SCHOOL",11311,"NEWPORT MILL RD","KENSINGTON","MONTGOMERY","MD","US",39.0416107,-77.06884708
1043: "SAFEWAY",10541,"HOWARD AVE","KENSINGTON","MONTGOMERY","MD","US",39.02822438,-77.0755196
1044: "HAIR CUTTERY",3731,"CONNECTICUT AVE","KENSINGTON","MONTGOMERY","MD","US",39.03323865,-77.07368044
1045: "STROSNIDERS",10504,"CONNECTICUT AVE","KENSINGTON","MONTGOMERY","MD","US",39.02781493,-77.07740792
1046: "",8616,"SAVANNAH RIVER RD","LAUREL","ANNE ARUNDEL","MD","US",39.100869,-76.812162
1047: "DOWNS PARK",,"CHESAPEAKE BAY DRIVE","PASADENA","ANNE ARUNDEL","MD","US",39.110711,-76.434062
1048: "",1559,"GUERDON CT","PASADENA","ANNE ARUNDEL","MD","US",39.102637,-76.456384
1049: "ARCOLA HEALTH AND REHABILITATION CENTER",901,"ARCOLA AVE","SILVER SPRING","MONTGOMERY","MD","US",39.036439,-77.025502
1050: "",9904,"GARDINER AVE","SILVER SPRING","MONTGOMERY","MD","US",39.017633,-77.049551
1051: "CVS",9520,"GEORGIA AVE","SILVER SPRING","MONTGOMERY","MD","US",39.010801,-77.041771
1052: "FOREST GLEN MEDICAL CENTER",9801,"GEORGIA AVE","SILVER SPRING","MONTGOMERY","MD","US",39.016042,-77.042148
1053: "",10009,"GREELEY AVE","SILVER SPRING","MONTGOMERY","MD","US",39.019575,-77.047453
1054: "ADVENTIST HOSPITAL",11886,"HEALING WAY","SILVER SPRING","MONTGOMERY","MD","US",39.049570,-76.956882
1055: "",2232,"HILDAROSE DR","SILVER SPRING","MONTGOMERY","MD","US",39.019385,-77.049779,
1056: "LA CASITA PUPESERIA AND MARKET",8214,"PINEY BRANCH RD","SILVER SPRING","MONTGOMERY","MD","US",38.993369,-77.009501
1057: "NOAA LIBRARY",1315,"EAST-WEST HIGHWAY","SILVER SPRING","MONTGOMERY","MD","US",38.991667,-77.030473
1058: "SNIDERS",1936,"SEMINARY RD","SILVER SPRING","MONTGOMERY","MD","US",39.0088797,-77.04162824
1059: "",1954,"SEMINARY RD","SILVER SPRING","MONTGOMERY","MD","US",39.008961,-77.04303
1060: "",1956,"SEMINARY RD","SILVER SPRING","MONTGOMERY","MD","US",39.008845,-77.043317
1061: "",9315,"WARREN ST","SILVER SPRING","MONTGOMERY","MD","US",39.00881,-77.048953
1062: "",9411,"WARREN ST","SILVER SPRING","MONTGOMERY","MD","US",39.010447,-77.048548
1063: "SILVER DINER",12276,"ROCKVILLE PK","ROCKVILLE","MONTGOMERY","MD","US",39.05798753,-77.12165374
1064: "",1605,"VIERS MILL RD","ROCKVILLE","MONTGOMERY","MD","US",39.07669788,-77.12306436
1065: "",1406,"LANGBROOK PL","ROCKVILLE","MONTGOMERY","MD","US",39.075583,-77.123833
1066: "",2225,"FOREST GLEN RD","SILVER SPRING","MONTGOMERY","MD","US",39.015394,-77.048357
1067: "BP",2601,"FOREST GLEN RD","SILVER SPRING","MONTGOMERY","MD","US",39.0147541,-77.05466857
1068: "OMEGA STUDIOS",12412,,"ROCKVILLE","MONTGOMERY","MD","US",39.06412645,-77.11252263
1069: "",10424,"43RD AVE","BELTSVILLE","PRINCE GEORGE","MD","US",39.033075,-76.923859
1070: "NASA",,"TIROS RD","GREENBELT","PRINCE GEORGE","MD","US",38.996764,-76.849323
1071: "",7001,"CRADLEROCK FARM COURT","COLUMBIA","HOWARD","MD","US",39.190009,-76.841152
1072: "BANGOR AIRPORT",,"GODFREY BOULEVARD","BANGOR","PENOBSCOT","ME","US",44.406700,-68.597114
1073: "",86,"ALLEN POINT LANE","BLUE HILLS","HANCOCK","ME","US",44.35378018,-68.57383976
1074: "TRADEWINDS",15,"SOUTH STREET","BLUE HILLS","HANCOCK","ME","US",44.40670019,-68.59711438
1075: "RITE AID",17,"SOUTH STREET","BLUE HILLS","HANCOCK","ME","US",44.40662476,-68.59610059
1076: "",880,"SOUTH GREENSFERRY RD","COUER D'ALENE","KOOTENAI","ID","US",47.693615,-116.915357
1077: "",898,"SOUTH GREENSFERRY RD","COUER D'ALENE","KOOTENAI","ID","US",47.69556,-116.91564
1078: "",,"DOUGLAS AVE","FORT WAYNE","ALLEN","IN","US",41.074247,-85.138531
1079: "JOHN GLENN AIRPORT",4600,,"COLUMBUS","FRANKLIN","OH","US",39.997959,-82.88132
1080: "MIDDLE RIDGE PLAZA",,,"AMHERST","LOHRAIN","OH","US",41.379695,-82.222877
1081: "RESIDENCE INN BY MARRIOTT",6364,"FRANTZ RD","DUBLIN",,"OH","US",40.097097,-83.123745
1082: "TOWPATH TRAVEL PLAZA",,,"BROADVIEW HEIGHTS","CUYAHOGA","OH","US",41.291654,-81.675815
1083: "NEW STANTON SERVICE PLAZA",,,"HEMPFIELD",,"PA","US",40.206267,-79.565682
1084: "",,"","LITITZ","LANCASTER","PA","US",40.154989, -76.304266
1085: "SOUTH SOMERSET SERVICE PLAZA",,,"SOMERSET","SOMERSET","PA","US",39.999154,-79.046526
1086: "HUNTLEY MEADOWS PARK",3701,"LOCKHEED BLVD","ALEXANDRIA","","VA","US",38.75422, -77.1058666666667
1087: "SHENANDOAH COOL SPRINGS BATTLEFIELD",,"","BLUEMONT","CLARKE","VA","US",39.142146,-77.866468
1088: "",14900,"CONFERENCE CENTER DR","CHANTILLY","FAIRFAX","VA","US",38.873934,-77.461939
1089: "THE PURE PASTY COMPANY",128C,"MAPLE AVE W","VIENNA","FAIRFAX","VA","US",44.40662476,-68.59610059
1090: "DIRT FARM BREWERY",18701,"FOGGY BOTTOM RD","BLUEMONT","LOUDON","VA","US",39.099655,-77.836975
1091: "",404,"BRINDLEY PL SW","LEESBURG","LOUDOUN","VA","US",39.092207,-77.591987
1092: "",818,"FERNDALE TERRACE NE","LEESBURG","LOUDOUN","VA","US",39.124843,-77.535445
1093: "",,"OATLANDS PLANTATION LN","OATLANDS","LOUDOUN","VA","US",39.04071,-77.61682
1094: "",,"PURCELLVILLE GATEWAY DR","PURCELLVILLE","LOUDOUN","VA","US",39.136193,-77.693198
1095: "THE CAPITAL GRILLE RESTAURANT",1861,,"MCLEAN","FAIRFAX","VA","US",38.915635,-77.22573
1096: "",,"","COLONIAL BEACH","WESTMORELAND","VA","US",38.25075,-76.9602533333333