TER1 (Statement): 100.00%
TER2 (Branch): 97.06%
TER3 (LCSAJ): 100.0% (6/6)
Approximate LCSAJ segments: 35
● Covered — this LCSAJ path was executed during testing.
● Not covered — this LCSAJ path was never executed. These are the paths to focus on.
Multiple dots on a line indicate that multiple control-flow paths begin at that line. Hovering over any dot shows:
start → end → jump
Uncovered paths show [NOT COVERED] in the tooltip.
1: package Geo::Address::Parser::Country; 2: 3: use strict; 4: use warnings; 5: 6: use Carp qw(croak carp); 7: use Locale::Object::Country; 8: use Object::Configure; 9: use Params::Get; 10: use Params::Validate::Strict qw(validate_strict); 11: use Return::Set qw(set_return); 12: 13: our $VERSION = '0.04'; 14: 15: # Direct component-to-country mappings, keyed on lowercase component. 16: # Values are either a plain country string, or a hashref with 17: # 'country' and optional 'warning' keys. 18: my %DIRECT = ( 19: 'england' => 'United Kingdom', 20: 'scotland' => 'United Kingdom', 21: 'wales' => 'United Kingdom', 22: 'isle of man' => 'United Kingdom', 23: 'northern ireland' => 'United Kingdom', 24: 'uk' => 'United Kingdom', 25: 'england, uk' => 'United Kingdom', 26: 'england uk' => 'United Kingdom', 27: 28: # Malformed but seen in real data 29: 'scot' => { 30: country => 'United Kingdom', 31: warning => "country should be 'Scotland' not 'Scot'", 32: }, 33: 34: # US variants 35: 'usa' => 'United States', 36: 'us' => 'United States', 37: 'u.s.a.' => 'United States', 38: 'united states of america' => 'United States', 39: 'united states' => 'United States', 40: 41: # German historical names 42: 'preussen' => 'Germany', 43: "preu\x{00DF}en" => 'Germany', 44: 'deutschland' => 'Germany', 45: 46: # Dutch variants 47: 'holland' => 'Netherlands', 48: 'the netherlands' => 'Netherlands', 49: # 'nl' => { # Not a common abbreviation and clashes with Newfoundland in Canada 50: # country => 'Netherlands', 51: # warning => 'assuming country is Netherlands', 52: # }, 53: 54: # Slovenian historical name 55: 'slovenija' => 'Slovenia', 56: 57: # Canadian provinces/territories missing their country 58: 'nova scotia' => { 59: country => 'Canada', 60: warning => "country 'Canada' missing from record", 61: }, 62: 'newfoundland' => { 63: country => 'Canada', 64: warning => "country 'Canada' missing from record", 65: }, 66: 'nfld' => { 67: country => 'Canada', 68: warning => "country 'Canada' missing from record", 69: }, 70: 'ns' => { 71: country => 'Canada', 72: warning => "country 'Canada' missing from record", 73: }, 74: 'can.' => { 75: country => 'Canada', 76: warning => "country 'Canada' missing from record", 77: }, 78: ); 79: 80: # Schema for new() arguments, used by Params::Validate::Strict 81: my $NEW_SCHEMA = { 82: us => { type => 'object' }, 83: ca_en => { type => 'object' }, 84: ca_fr => { type => 'object' }, 85: au => { type => 'object' }, 86: geonames => { 87: type => 'object', 88: # can => 'search', # Geo::GeoNames is broken, it uses AUTOLOAD. I must fix that. 89: optional => 1, 90: }, 91: }; 92: 93: # Schema for resolve() arguments. 94: # component is optional: when absent, resolve() extracts it from place 95: # automatically as the last comma-separated token. 96: my $RESOLVE_SCHEMA = { 97: place => { type => 'string', min => 1 }, 98: component => { type => 'string', min => 1, optional => 1 }, 99: }; 100: 101: # Schema for the resolve() return value 102: my $RESOLVE_RETURN_SCHEMA = { 103: type => 'hashref', 104: schema => { 105: country => { type => 'string', optional => 1 }, 106: place => { type => 'string', min => 1 }, 107: warnings => { type => 'arrayref' }, 108: unknown => { type => 'boolean' }, 109: }, 110: }; 111: 112: # Schema for normalise_place() arguments 113: my $NORMALISE_SCHEMA = { 114: place => { type => 'string', min => 1 }, 115: }; 116: 117: # Schema for the normalise_place() return value 118: my $NORMALISE_RETURN_SCHEMA = { 119: type => 'hashref', 120: schema => { 121: place => { type => 'string', min => 1 }, 122: warnings => { type => 'arrayref' }, 123: }, 124: }; 125: 126: =head1 NAME 127: 128: Geo::Address::Parser::Country - Resolve a place string component to a 129: canonical country name 130: 131: =head1 VERSION 132: 133: Version 0.04 134: 135: =head1 SYNOPSIS 136: 137: use Geo::Address::Parser::Country; 138: use Locale::US; 139: use Locale::CA; 140: use Locale::AU; 141: 142: my $resolver = Geo::Address::Parser::Country->new({ 143: us => Locale::US->new(), 144: ca_en => Locale::CA->new(lang => 'en'), 145: ca_fr => Locale::CA->new(lang => 'fr'), 146: au => Locale::AU->new(), 147: }); 148: 149: # Simple form: component extracted automatically from place 150: my $result = $resolver->resolve( 151: place => 'Ramsgate, Kent, England', 152: ); 153: 154: # Explicit form: caller supplies the component directly 155: my $result = $resolver->resolve( 156: component => 'England', 157: place => 'Ramsgate, Kent, England', 158: ); 159: 160: # $result->{country} eq 'United Kingdom' 161: # $result->{place} eq 'Ramsgate, Kent, England' 162: # $result->{warnings} is [] 163: # $result->{unknown} is 0 164: 165: =head1 DESCRIPTION 166: 167: Resolves the last comma-separated component of a place string into a 168: canonical country name. Handles common variants, abbreviations, and 169: historical names found in genealogy data and other poorly-normalised 170: address sources. 171: 172: Designed specifically to tolerate poor-quality data from software 173: imports where place strings may be inconsistent, abbreviated, or use 174: historical country names no longer in common use. 175: 176: Resolution proceeds through the following steps in order: 177: 178: =over 4 179: 180: =item 1. Direct lookup table (covers historical names, abbreviations, 181: common variants) 182: 183: =item 2. US state code or name via Locale::US 184: 185: =item 3. Canadian province code or name via Locale::CA (English and French) 186: 187: =item 4. Australian state code or name via Locale::AU 188: 189: =item 5. Locale::Object::Country by name 190: 191: =item 6. Geo::GeoNames search (optional, only if object provided at 192: construction) 193: 194: =item 7. Unknown - returns with C<unknown =E<gt> 1> 195: 196: =back 197: 198: =head1 TODO 199: 200: =over 4 201: 202: =item * Complete C<normalise_place()> to handle missing commas before 203: country and state names in raw uncleaned input strings. Poor data 204: import means strings like C<"Houston TX USA"> or 205: C<"Some Place England"> need comma insertion before component 206: extraction can work correctly. This should be called before 207: C<resolve()> for raw uncleaned input. 208: 209: =back 210: 211: =head1 METHODS 212: 213: =head2 new 214: 215: =head3 Purpose 216: 217: Constructs a new resolver object. The locale objects are used for 218: state and province lookups and are retained for the lifetime of the 219: object. 220: 221: =head3 API Specification 222: 223: =head4 Input 224: 225: { 226: us => { type => 'object' }, # Locale::US instance 227: ca_en => { type => 'object' }, # Locale::CA English instance 228: ca_fr => { type => 'object' }, # Locale::CA French instance 229: au => { type => 'object' }, # Locale::AU instance 230: geonames => { # Optional Geo::GeoNames instance 231: type => 'object', 232: optional => 1, 233: }, 234: } 235: 236: =head4 Output 237: 238: { type => 'object', isa => 'Geo::Address::Parser::Country' } 239: 240: =head3 Arguments 241: 242: =over 4 243: 244: =item * C<us> - A L<Locale::US> instance. Required. 245: 246: =item * C<ca_en> - A L<Locale::CA> instance with C<lang =E<gt> 'en'>. Required. 247: 248: =item * C<ca_fr> - A L<Locale::CA> instance with C<lang =E<gt> 'fr'>. Required. 249: 250: =item * C<au> - A L<Locale::AU> instance. Required. 251: 252: =item * C<geonames> - An optional L<Geo::GeoNames> instance used as a 253: last-resort fallback when all other resolution methods fail. 254: 255: =back 256: 257: =head3 Returns 258: 259: A blessed C<Geo::Address::Parser::Country> object. 260: 261: =head3 Side Effects 262: 263: None. 264: 265: =head3 Notes 266: 267: The locale objects are stored by reference and shared for all calls to 268: C<resolve()>. Constructing them once and reusing the resolver object 269: is more efficient than constructing a new resolver for each lookup. 270: 271: C<Object::Configure> is used after validation to allow locale objects 272: to be supplied via environment variables or a config file rather than 273: always being passed explicitly. 274: 275: =head3 Example 276: 277: my $resolver = Geo::Address::Parser::Country->new({ 278: us => Locale::US->new(), 279: ca_en => Locale::CA->new(lang => 'en'), 280: ca_fr => Locale::CA->new(lang => 'fr'), 281: au => Locale::AU->new(), 282: }); 283: 284: =cut 285: 286: sub new { 287: my $class = shift; 288: 289: # Accept both hashref and flat list via Params::Get 290: my $args = Params::Get::get_params(undef, @_); 291: 292: # Validate all constructor arguments strictly 293: validate_strict({ 294: description => 'Geo::Address::Parser::Country::new', 295: input => $args, 296: schema => $NEW_SCHEMA, 297: }); 298: 299: $args = Object::Configure::configure($class, $args); 300: 301: # Build and return the blessed object 302: return bless { 303: us => $args->{us}, 304: ca_en => $args->{ca_en}, 305: ca_fr => $args->{ca_fr}, 306: au => $args->{au}, 307: geonames => $args->{geonames}, 308: }, $class; 309: } 310: 311: =head2 resolve 312: 313: =head3 Purpose 314: 315: Resolves the last comma-separated component of a place string to a 316: canonical country name, and returns the (possibly modified) place 317: string alongside any warnings generated during resolution. 318: 319: =head3 API Specification 320: 321: =head4 Input 322: 323: { 324: place => { type => 'string', min => 1 }, # required 325: component => { type => 'string', min => 1, optional => 1 }, 326: } 327: 328: =head4 Output 329: 330: { 331: type => 'hashref', 332: schema => { 333: country => { type => 'string', optional => 1 }, 334: place => { type => 'string', min => 1 }, 335: warnings => { type => 'arrayref' }, 336: unknown => { type => 'boolean' }, 337: }, 338: } 339: 340: =head3 Arguments 341: 342: =over 4 343: 344: =item * C<place> - The full place string, e.g. 345: C<"Ramsgate, Kent, England">. Required. May be modified by appending a 346: country suffix where needed. 347: 348: =item * C<component> - The last comma-separated component of the place 349: string, e.g. C<"England">, C<"TX">, C<"NSW">. Optional. When absent, 350: C<resolve()> extracts it automatically as the last comma-separated 351: token of C<place>. When C<place> contains no comma, the entire 352: C<place> string is used as the component. Supplying C<component> 353: explicitly is useful when the caller already has it available from a 354: structured data source. 355: 356: =back 357: 358: =head3 Returns 359: 360: A hashref containing: 361: 362: =over 4 363: 364: =item * C<country> - The canonical country name as a string, e.g. 365: C<"United Kingdom">. C<undef> if resolution failed. 366: 367: =item * C<place> - The full place string, possibly with a country 368: suffix appended (e.g. C<", USA">). Always returned even if unmodified. 369: 370: =item * C<warnings> - An arrayref of warning strings generated during 371: resolution. May be empty. The caller is responsible for acting on 372: these, e.g. by passing them to a C<complain()> function. 373: 374: =item * C<unknown> - A boolean. True if the country could not be 375: resolved by any method. 376: 377: =back 378: 379: =head3 Side Effects 380: 381: None. All warnings are returned to the caller rather than emitted 382: directly. 383: 384: =head3 Notes 385: 386: Resolution order is: direct lookup, US state, Canadian province, 387: Australian state, Locale::Object::Country, GeoNames (if available). 388: The first successful match wins. 389: 390: When a US state, Canadian province, or Australian state is recognised, 391: the appropriate country string (C<", USA">, C<", Canada">, 392: C<", Australia">) is appended to C<place> if not already present. 393: 394: =head3 Example 395: 396: # Simple form - component extracted automatically 397: my $result = $resolver->resolve( 398: place => 'Houston, TX', 399: ); 400: 401: # Explicit form - component supplied by caller 402: my $result = $resolver->resolve( 403: component => 'TX', 404: place => 'Houston, TX', 405: ); 406: 407: # $result->{country} eq 'United States' 408: # $result->{place} eq 'Houston, TX, USA' 409: # $result->{warnings}[0] eq 'TX: assuming country is United States' 410: # $result->{unknown} is 0 411: 412: =cut 413: 414: sub resolve { ●415 → 447 → 516●415 → 447 → 0 415: my $self = shift; 416: 417: # Accept both hashref and flat list 418: my $args = Params::Get::get_params(undef, @_); 419: 420: # Validate input arguments 421: validate_strict({ 422: description => 'Geo::Address::Parser::Country::resolve', 423: input => $args, 424: schema => $RESOLVE_SCHEMA, 425: }); 426: 427: my $place = $args->{place}; 428: 429: # Extract the component from the place string when the caller has not 430: # supplied it explicitly. The component is the last comma-separated 431: # token, trimmed of surrounding whitespace. When the place string 432: # contains no comma at all, the entire string is the component. 433: my $component = $args->{component} 434: // do { 435: my ($c) = $place =~ /(?:^|,)\s*([^,]+?)\s*$/; 436: $c // $place; 437: }; 438: 439: my $lc = lc($component); 440: 441: # Accumulate warnings to return to caller rather than emitting them 442: my @warnings; 443: my $country; 444: 445: # Step 1: check the direct lookup table first, handles historical 446: # names, common abbreviations and malformed but real-world input 447: if(my $match = $DIRECT{$lc}) {Mutants (Total: 1, Killed: 1, Survived: 0)
448: if(ref($match) eq 'HASH') {
Mutants (Total: 1, Killed: 1, Survived: 0)
449: $country = $match->{country}; 450: push @warnings, $match->{warning} if $match->{warning}; 451: } else { 452: $country = $match; 453: } 454: } 455: 456: # Step 2: two-letter US state code, e.g. "TX" 457: elsif($component =~ /^[A-Z]{2}$/i 458: && $self->{us}{code2state}{uc($component)}) { 459: $country = 'United States'; 460: push @warnings, "$component: assuming country is United States"; 461: $place = $self->_append_country($place, 'USA'); 462: } 463: 464: # Step 3: US state full name, e.g. "Texas" 465: elsif($self->{us}{state2code}{uc($component)}) { 466: $country = 'United States'; 467: push @warnings, "$component: assuming country is United States"; 468: $place = $self->_append_country($place, 'USA'); 469: } 470: 471: # Step 4: Canadian province code in English or French 472: elsif($component =~ /^[A-Z]{2}$/i 473: && ($self->{ca_en}{code2province}{uc($component)} 474: || $self->{ca_fr}{code2province}{uc($component)})) { 475: $country = 'Canada'; 476: push @warnings, "$component: assuming country is Canada"; 477: $place = $self->_append_country($place, 'Canada'); 478: } 479: 480: # Step 5: Canadian province full name in English or French 481: elsif($self->{ca_en}{province2code}{uc($component)} 482: || $self->{ca_fr}{province2code}{uc($component)}) { 483: $country = 'Canada'; 484: push @warnings, "$component: assuming country is Canada"; 485: $place = $self->_append_country($place, 'Canada'); 486: } 487: 488: # Step 6: Australian state code, e.g. "NSW", "VIC" 489: elsif($component =~ /^[A-Z]{2,3}$/i 490: && $self->{au}{code2state}{$component}) { 491: $country = 'Australia'; 492: push @warnings, "$component: assuming country is Australia"; 493: $place = $self->_append_country($place, 'Australia'); 494: } 495: 496: # Step 7: Australian state full name 497: elsif($self->{au}{state2code}{uc($component)}) { 498: $country = 'Australia'; 499: push @warnings, "$component: assuming country is Australia"; 500: $place = $self->_append_country($place, 'Australia'); 501: } 502: 503: # Step 8: fall back to Locale::Object::Country by name 504: elsif(my $loc = Locale::Object::Country->new(name => $component)) { 505: $country = $loc->name(); 506: } 507: 508: # Step 9: optional GeoNames fallback for anything still unresolved 509: elsif($self->{geonames}) { 510: $country = $self->_geonames_lookup( 511: $place, $component, \@warnings 512: ); 513: } 514: 515: # Build and validate the return structure before handing back ●516 → 523 → 0 516: my $result = { 517: country => $country, 518: place => $place, 519: warnings => \@warnings, 520: unknown => defined($country) ? 0 : 1, 521: }; 522: 523: return set_return($result, $RESOLVE_RETURN_SCHEMA); 524: } 525: 526: =head2 normalise_place 527: 528: =head3 Purpose 529: 530: Inserts missing commas into a raw, uncleaned place string so that 531: C<resolve()> can reliably extract the last component. Raw input from 532: poor-quality data imports frequently omits the commas that separate 533: city, state, and country tokens. 534: 535: =head3 API Specification 536: 537: =head4 Input 538: 539: { 540: place => { type => 'string', min => 1 }, 541: } 542: 543: =head4 Output 544: 545: { 546: type => 'hashref', 547: schema => { 548: place => { type => 'string', min => 1 }, 549: warnings => { type => 'arrayref' }, 550: }, 551: } 552: 553: =head3 Arguments 554: 555: =over 4 556: 557: =item * C<place> - The raw place string to normalise, e.g. 558: C<"Houston TX USA"> or C<"Some Place England">. Required. 559: 560: =back 561: 562: =head3 Returns 563: 564: A hashref containing: 565: 566: =over 4 567: 568: =item * C<place> - The normalised place string with commas inserted 569: where they were missing, e.g. C<"Houston, TX, USA">. Always returned 570: even if no changes were made. 571: 572: =item * C<warnings> - An arrayref of warning strings generated during 573: normalisation, e.g. noting where commas were inserted. May be empty. 574: 575: =back 576: 577: =head3 Side Effects 578: 579: None. 580: 581: =head3 Notes 582: 583: This method is not yet fully implemented. It currently returns the 584: place string unchanged. Implementation requires scanning the token 585: sequence against the locale tables (US states, Canadian provinces, 586: Australian states, and the %DIRECT country table) to identify where 587: comma boundaries belong. 588: 589: Call this method before C<resolve()> when working with raw input that 590: may lack commas: 591: 592: my $norm = $resolver->normalise_place(place => 'Houston TX USA'); 593: my $result = $resolver->resolve(place => $norm->{place}); 594: 595: =head3 Example 596: 597: my $norm = $resolver->normalise_place(place => 'Some Place England'); 598: # $norm->{place} eq 'Some Place, England' (once implemented) 599: # $norm->{warnings} contains a note about comma insertion 600: 601: =cut 602: 603: sub normalise_place { 604: my $self = shift; 605: 606: # Accept both hashref and flat list 607: my $args = Params::Get::get_params(undef, @_); 608: 609: # Validate input arguments 610: validate_strict({ 611: description => 'Geo::Address::Parser::Country::normalise_place', 612: input => $args, 613: schema => $NORMALISE_SCHEMA, 614: }); 615: 616: my $place = $args->{place}; 617: my @warnings; 618: 619: # TODO: scan tokens against locale tables and %DIRECT, inserting 620: # commas where boundaries are identified. For example: 621: # 'Houston TX USA' -> 'Houston, TX, USA' 622: # 'Some Place England' -> 'Some Place, England' 623: # This requires iterating right-to-left through whitespace-separated 624: # tokens, matching each against known country/state/province names, 625: # and inserting a comma immediately before the first match found. 626: 627: my $result = { 628: place => $place, 629: warnings => \@warnings, 630: }; 631: 632: return set_return($result, $NORMALISE_RETURN_SCHEMA); 633: } 634: 635: # _append_country 636: # 637: # Purpose: 638: # Appends a country suffix to a place string if not already present. 639: # 640: # Entry criteria: 641: # $self - blessed object 642: # $place - non-empty place string 643: # $suffix - country string to append, e.g. 'USA' 644: # 645: # Exit status: 646: # Returns the (possibly modified) place string. 647: # 648: # Side effects: 649: # None. 650: # 651: # Notes: 652: # Uses a case-insensitive check to avoid double-appending. 653: # E.g. 'Houston, TX' becomes 'Houston, TX, USA'. 654: # 'Houston, TX, USA' is returned unchanged. 655: 656: sub _append_country { 657: my ($self, $place, $suffix) = @_; 658: 659: # Return unchanged if suffix already present at end of string 660: return $place if $place =~ /,\s*\Q$suffix\E\s*$/i;
Mutants (Total: 2, Killed: 2, Survived: 0)
661: 662: return "$place, $suffix";
Mutants (Total: 2, Killed: 2, Survived: 0)
663: } 664: 665: # _geonames_lookup 666: # 667: # Purpose: 668: # Uses the optional Geo::GeoNames object to search for a country 669: # name when all other resolution methods have failed. 670: # 671: # Entry criteria: 672: # $self - blessed object with {geonames} set 673: # $place - full place string, used as the search query for 674: # maximum context 675: # $component - original component string, used in warning text 676: # $warnings - arrayref to push warnings onto 677: # 678: # Exit status: 679: # Returns the country name string on success, undef on failure. 680: # 681: # Side effects: 682: # Pushes a warning onto $warnings if a country is found via GeoNames. 683: # 684: # Notes: 685: # We search on the full place string rather than just the component 686: # to give GeoNames maximum context and improve result accuracy. 687: # The first result is used. 688: 689: sub _geonames_lookup { ●690 → 699 → 706●690 → 699 → 0 690: my ($self, $place, $component, $warnings) = @_; 691: 692: my $result = $self->{geonames}->search( 693: q => $place, 694: style => 'FULL', 695: ); 696: 697: # Normalise to the first element if an arrayref was returned. 698: # Guard against a bare scalar (e.g. an error string) being returned. 699: if(ref($result) eq 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
700: $result = $result->[0]; 701: } elsif(!ref($result)) { 702: return; # scalar return is not a hashref â bail out cleanly 703: } 704: 705: # Must be a plain hashref at this point ●706 → 715 → 0 706: return unless ref($result) eq 'HASH'; 707: 708: # Extract country name; must be a defined, non-ref, non-empty string 709: my $country = $result->{countryName}; 710: return unless defined($country) 711: && !ref($country) 712: && length($country); 713: 714: push @{$warnings}, "$component: assuming country is $country"; 715: return $country;
Mutants (Total: 2, Killed: 2, Survived: 0)
716: } 717: 718: =head1 AUTHOR 719: 720: Nigel Horne C<< <njh@nigelhorne.com> >> 721: 722: =head1 REPOSITORY 723: 724: L<https://github.com/nigelhorne/Geo-Address-Parser-Country> 725: 726: =head1 SUPPORT 727: 728: This module is provided as-is without any warranty. 729: 730: Please report any bugs or feature requests to C<bug-geo-address-parser at rt.cpan.org>, 731: or through the web interface at 732: L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Geo-Address-Parser-Country>. 733: I will be notified, and then you'll 734: automatically be notified of progress on your bug as I make changes. 735: 736: =head1 BUGS 737: 738: =over 4 739: 740: =item * C<normalise_place()> is not yet implemented. It currently 741: returns the place string unchanged. See L</normalise_place> for 742: details of the planned behaviour. 743: 744: =item * The step 6 Australian state code lookup uses the raw, 745: un-normalised component as the hash key, making it case-sensitive 746: unlike steps 2-5. Lowercase codes such as C<nsw> will not match. 747: A fix to apply C<uc($component)> consistently is pending. 748: 749: =item * C<Geo::GeoNames> generates its query methods via C<AUTOLOAD>, 750: so C<can('search')> returns false at the Perl level even though 751: C<$geonames-E<gt>search(...)> works correctly at runtime. The 752: C<can =E<gt> 'search'> schema check has been commented out as a 753: temporary workaround pending a fix to C<Geo::GeoNames> itself. 754: 755: =back 756: 757: Please report additional bugs via the GitHub issue tracker: 758: L<https://github.com/nigelhorne/Geo-Address-Parser-Country/issues> 759: 760: =head1 SEE ALSO 761: 762: =over 4 763: 764: =item * L<Test Dashboard|https://nigelhorne.github.io/Geo-Address-Parser-Country/coverage/> 765: 766: =item * L<Geo::Address::Parser> 767: 768: =item * L<Locale::US> 769: 770: =item * L<Locale::CA> 771: 772: =item * L<Locale::AU> 773: 774: =item * L<Locale::Object::Country> 775: 776: =item * L<Geo::GeoNames> 777: 778: =item * L<Object::Configure> 779: 780: =item * L<Params::Get> 781: 782: =item * L<Params::Validate::Strict> 783: 784: =item * L<Return::Set> 785: 786: =back 787: 788: =head1 LICENCE AND COPYRIGHT 789: 790: Copyright 2026 Nigel Horne. 791: 792: Usage is subject to GPL2 licence terms. 793: If you use it, please let me know. 794: 795: =cut 796: 797: 1;