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