Address Geocode – International C# Code Snippet

using System.Threading.Tasks;

namespace address_geocode_international_dot_net.REST
{
    /// <summary>
    /// Client for PlaceSearch operation via ServiceObjects AGI REST API.
    /// Supports fallback to backup URL if live service fails.
    /// </summary>
    public static class PlaceSearchClient
    {
        // Base URL constants: production, backup, and trial
        private const string LiveBaseUrl = "https://sws.serviceobjects.com/AGI/api.svc/json/PlaceSearch";
        private const string BackupBaseUrl = "https://swsbackup.serviceobjects.com/AGI/api.svc/json/PlaceSearch";
        private const string TrialBaseUrl = "https://trial.serviceobjects.com/AGI/api.svc/json/PlaceSearch";

        /// <summary>
        /// Synchronously call the PlaceSearchJson endpoint.
        /// </summary>
        /// <param name="input">Request parameters (address, license key, isLive).</param>
        /// <returns>Deserialized <see cref="AGIPlaceSearchResponse"/>.</returns>
        public static AGIPlaceSearchResponse Invoke(PlaceSearchInput input)
        {
            // Use appropriate base URL depending on live/trial flag
            var url = BuildUrl(input, input.IsLive ? LiveBaseUrl : TrialBaseUrl);

            //Use query string parameters so missing/options fields don't break
            //the URL as path parameters would.
            AGIPlaceSearchResponse response = Helper.HttpGet<AGIPlaceSearchResponse>(url, input.TimeoutSeconds);

            // Fallback on error payload in live mode
            if (input.IsLive && !IsValid(response))
            {
                var fallbackUrl = BuildUrl(input, BackupBaseUrl);
                AGIPlaceSearchResponse fallbackResponse = Helper.HttpGet<AGIPlaceSearchResponse>(fallbackUrl, input.TimeoutSeconds);
                return fallbackResponse;
            }

            return response;
        }

        /// <summary>
        /// Asynchronously call the PlaceSearchJson endpoint.
        /// </summary>
        /// <param name="input">Request parameters (address, license key, isLive).</param>
        /// <returns>Deserialized <see cref="AGIPlaceSearchResponse"/>.</returns>
        public static async Task<AGIPlaceSearchResponse> InvokeAsync(PlaceSearchInput input)
        {
            // Use appropriate base URL depending on live/trial flag
            var url = BuildUrl(input, input.IsLive ? LiveBaseUrl : TrialBaseUrl);

            // Perform HTTP GET request asynchronously and deserialize response
            AGIPlaceSearchResponse response = await Helper.HttpGetAsync<AGIPlaceSearchResponse>(url, input.TimeoutSeconds).ConfigureAwait(false);

            // In live mode, attempt backup URL if primary response is invalid
            if (input.IsLive && !IsValid(response))
            {
                var fallbackUrl = BuildUrl(input, BackupBaseUrl);
                AGIPlaceSearchResponse fallbackResponse = await Helper.HttpGetAsync<AGIPlaceSearchResponse>(fallbackUrl, input.TimeoutSeconds).ConfigureAwait(false);
                return fallbackResponse;
            }

            return response;
        }

        /// <summary>
        /// Build the full request URL with encoded query string.
        /// </summary>
        /// <param name="input">Input data for PlaceSearch.</param>
        /// <param name="baseUrl">Base endpoint URL.</param>
        /// <returns>Complete request URL.</returns>
        private static string BuildUrl(PlaceSearchInput input, string baseUrl)
        {
            return baseUrl + "?" +
                   $"SingleLine={Helper.UrlEncode(input.SingleLine)}" +
                   $"Address1={Helper.UrlEncode(input.Address1)}" +
                   $"Address2={Helper.UrlEncode(input.Address2)}" +
                   $"Address3={Helper.UrlEncode(input.Address3)}" +
                   $"Address4={Helper.UrlEncode(input.Address4)}" +
                   $"Address5={Helper.UrlEncode(input.Address5)}" +
                   $"Locality={Helper.UrlEncode(input.Locality)}" +
                   $"AdministrativeArea={Helper.UrlEncode(input.AdministrativeArea)}" +
                   $"PostalCode={Helper.UrlEncode(input.PostalCode)}" +
                   $"&Country={Helper.UrlEncode(input.Country)}" +
                   $"&Boundaries={Helper.UrlEncode(input.Boundaries)}" +
                   $"&MaxResults={Helper.UrlEncode(input.MaxResults)}" +
                   $"&SearchType={Helper.UrlEncode(input.SearchType)}" +
                   $"&Extras={Helper.UrlEncode(input.Extras)}" +
                   $"&LicenseKey={Helper.UrlEncode(input.LicenseKey)}";
        }

        /// <summary>
        /// Checks if the response is valid (non-null and no error).
        /// </summary>
        /// <param name="response">Response to validate.</param>
        /// <returns>True if valid; otherwise false.</returns>
        private static bool IsValid(AGIPlaceSearchResponse response) => response?.Error == null || response.Error.TypeCode != "3";


        /// <summary>
        /// Input parameters for the PlaceSearch operation.
        /// </summary>
        /// <param name="SingleLine">Single-line address input. - Optional</param>
        /// <param name="Address1">Address line 1. - Optional</param>
        /// <param name="Address2">Address line 2. - Optional</param>
        /// <param name="Address3">Address line 3. - Optional</param>
        /// <param name="Address4">Address line 4. - Optional</param>
        /// <param name="Address5">Address line 5. - Optional</param>
        /// <param name="Locality">City or locality. - Optional</param>
        /// <param name="AdministrativeArea">State or province. - Optional</param>
        /// <param name="PostalCode">Zip or postal code. - Optional</param>
        /// <param name="Country">Country ISO code (e.g., "US", "CA"). - Optional</param>
        /// <param name="Boundaries">Geolocation search boundaries. - Optional</param>
        /// <param name="MaxResults">Maximum number of results to return. - Optional</param>
        /// <param name="SearchType">Specifies place type to search for. - Optional</param>
        /// <param name="Extras">Additional search attributes. - Optional</param>
        /// <param name="LicenseKey">Service Objects license key. - Required</param>
        /// <param name="IsLive">True to use production+backup; false for trial only. - Required</param>
        /// <param name="TimeoutSeconds">Request timeout in seconds (default: 15).</param>
        public record PlaceSearchInput(
            string SingleLine = "",
            string Address1 = "",
            string Address2 = "",
            string Address3 = "",
            string Address4 = "",
            string Address5 = "",
            string Locality = "",
            string AdministrativeArea = "",
            string PostalCode = "",
            string Country = "",
            string Boundaries = "",
            string MaxResults = "",
            string SearchType = "",
            string Extras = "",
            string LicenseKey = "",
            bool IsLive = true,
            int TimeoutSeconds = 15
        );
    }
}


using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;

namespace address_geocode_international_dot_net.REST
{
    /// <summary>
    /// Represents the AGI ReverseSearch API response (REST).
    /// </summary>
    [DataContract]
    public class AGIReverseSearchResponse
    {
        public AGIReverseSearchResponse()
        {
            Locations = new List<Location>();
        }

        [DataMember(Name = "SearchInfo")]
        public SearchInfo? SearchInfo { get; set; }

        [DataMember(Name = "Locations")]
        public List<Location> Locations { get; set; }

        [DataMember(Name = "Error")]
        public ErrorDetails? Error { get; set; }

        public override string ToString()
        {
            string output = "AGI Reverse Search Response:\n";
            output += SearchInfo != null ? SearchInfo.ToString() : "  SearchInfo: null\n";

            output += Locations != null && Locations.Count > 0
                ? "  Locations:\n" +
                    string.Join("", Locations.Select((loc, i) => $"    [{i + 1}]:\n{loc}"))
                : "  Locations: []\n";

            output += Error != null ? Error.ToString() : "  Error: null\n";
            return output;
        }
    }

    /// <summary>
    /// Represents the AGI PlaceSearch API response (REST).
    /// </summary>
    [DataContract]
    public class AGIPlaceSearchResponse
    {
        [DataMember(Name = "SearchInfo")]
        public SearchInfo? SearchInfo { get; set; }

        [DataMember(Name = "Locations")]
        public Location[]? Locations { get; set; }

        [DataMember(Name = "Error")]
        public ErrorDetails? Error { get; set; }

        public override string ToString()
        {
            string output = "AGI Place Search Response:\n";
            output += SearchInfo != null ? SearchInfo.ToString() : "  SearchInfo: null\n";

            if (Locations != null && Locations.Length > 0)
            {
                output += "  Locations:\n";
                for (int i = 0; i < Locations.Length; i++)
                {
                    output += $"    [{i + 1}]:\n{Locations[i]}\n";
                }
            }
            else
            {
                output += "  Locations: []\n";
            }

            output += Error != null ? Error.ToString() : "  Error: null\n";
            return output;
        }
    }

    /// <summary>
    /// Search information block returned by the AGI service.
    /// </summary>
    [DataContract]
    public class SearchInfo
    {
        [DataMember(Name = "Status")]
        public string? Status { get; set; }

        [DataMember(Name = "NumberOfLocations")]
        public int? NumberOfLocations { get; set; }

        [DataMember(Name = "Notes")]
        public string? Notes { get; set; }

        [DataMember(Name = "NotesDesc")]
        public string? NotesDesc { get; set; }

        [DataMember(Name = "Warnings")]
        public string? Warnings { get; set; }

        [DataMember(Name = "WarningDesc")]
        public string? WarningDesc { get; set; }

        public override string ToString()
        {
            string output = "  SearchInfo:\n";
            output += $"    Status           : {Status}\n";
            output += $"    NumberOfLocations: {NumberOfLocations}\n";
            output += $"    Notes            : {Notes}\n";
            output += $"    NotesDesc        : {NotesDesc}\n";
            output += $"    Warnings         : {Warnings}\n";
            output += $"    WarningDesc      : {WarningDesc}\n";
            return output;
        }
    }

    /// <summary>
    /// Represents a geocoded location as returned by the AGI service.
    /// </summary>
    [DataContract]
    public class Location
    {
        public Location()
        {
            AddressComponents = new AddressComponents();
        }

        [DataMember(Name = "PrecisionLevel")]
        public double PrecisionLevel { get; set; }

        [DataMember(Name = "Type")]
        public string? Type { get; set; }

        [DataMember(Name = "Latitude")]
        public string? Latitude { get; set; }

        [DataMember(Name = "Longitude")]
        public string? Longitude { get; set; }

        [DataMember(Name = "AddressComponents")]
        public AddressComponents? AddressComponents { get; set; }

        [DataMember(Name = "PlaceName")]
        public string? PlaceName { get; set; }

        [DataMember(Name = "GoogleMapsURL")]
        public string? GoogleMapsURL { get; set; }

        [DataMember(Name = "BingMapsURL")]
        public string? BingMapsURL { get; set; }

        [DataMember(Name = "MapQuestURL")]
        public string? MapQuestURL { get; set; }

        [DataMember(Name = "StateFIPS")]
        public string? StateFIPS { get; set; }

        [DataMember(Name = "CountyFIPS")]
        public string? CountyFIPS { get; set; }

        [DataMember(Name = "ClassFP")]
        public string? ClassFP { get; set; }

        public override string ToString()
        {
            string output = "";
            output += $"      Type          : {Type}\n";
            output += $"      PrecisionLevel: {PrecisionLevel}\n";
            output += $"      Latitude      : {Latitude}\n";
            output += $"      Longitude     : {Longitude}\n";
            output += $"      PlaceName     : {PlaceName}\n";
            output += $"      GoogleMapsURL : {GoogleMapsURL}\n";
            output += $"      BingMapsURL   : {BingMapsURL}\n";
            output += $"      MapQuestURL   : {MapQuestURL}\n";
            output += $"      StateFIPS     : {StateFIPS}\n";
            output += $"      CountyFIPS    : {CountyFIPS}\n";
            output += $"      ClassFP       : {ClassFP}\n";
            output += AddressComponents != null
                ? AddressComponents.ToString()
                : "      AddressComponents: null\n";
            return output;
        }
    }

    /// <summary>
    /// Address components returned within a location result.
    /// </summary>
    [DataContract]
    public class AddressComponents
    {
        [DataMember(Name = "PremiseNumber")]
        public string? PremiseNumber { get; set; }

        [DataMember(Name = "Thoroughfare")]
        public string? Thoroughfare { get; set; }

        [DataMember(Name = "DoubleDependentLocality")]
        public string? DoubleDependentLocality { get; set; }

        [DataMember(Name = "DependentLocality")]
        public string? DependentLocality { get; set; }

        [DataMember(Name = "Locality")]
        public string? Locality { get; set; }

        [DataMember(Name = "AdministrativeArea1")]
        public string? AdministrativeArea1 { get; set; }

        [DataMember(Name = "AdministrativeArea1Abbreviation")]
        public string? AdministrativeArea1Abbreviation { get; set; }

        [DataMember(Name = "AdministrativeArea2")]
        public string? AdministrativeArea2 { get; set; }

        [DataMember(Name = "AdministrativeArea2Abbreviation")]
        public string? AdministrativeArea2Abbreviation { get; set; }

        [DataMember(Name = "AdministrativeArea3")]
        public string? AdministrativeArea3 { get; set; }

        [DataMember(Name = "AdministrativeArea3Abbreviation")]
        public string? AdministrativeArea3Abbreviation { get; set; }

        [DataMember(Name = "AdministrativeArea4")]
        public string? AdministrativeArea4 { get; set; }

        [DataMember(Name = "AdministrativeArea4Abbreviation")]
        public string? AdministrativeArea4Abbreviation { get; set; }

        [DataMember(Name = "PostalCode")]
        public string? PostalCode { get; set; }

        [DataMember(Name = "Country")]
        public string? Country { get; set; }

        [DataMember(Name = "CountryISO2")]
        public string? CountryISO2 { get; set; }

        [DataMember(Name = "CountryISO3")]
        public string? CountryISO3 { get; set; }

        [DataMember(Name = "GoogleMapsURL")]
        public string? GoogleMapsURL { get; set; }

        [DataMember(Name = "PlaceName")]
        public string? PlaceName { get; set; }

        [DataMember(Name = "IsUnincorporated")]
        public string? IsUnincorporated { get; set; }

        [DataMember(Name = "TimeZone_UTC")]
        public string? TimeZone_UTC { get; set; }

        [DataMember(Name = "CongressCode")]
        public string? CongressCode { get; set; }

        [DataMember(Name = "CensusTract ")]
        public string? CensusTract { get; set; }

        [DataMember(Name = "CensusGeoID ")]
        public string? CensusGeoID { get; set; }

        [DataMember(Name = "ClassFP")]
        public string? ClassFP { get; set; }

        [DataMember(Name = "StateFIPS ")]
        public string? StateFIPS { get; set; }

        [DataMember(Name = "SLDUST ")]
        public string? SLDUST { get; set; }

        [DataMember(Name = "SLDLST ")]
        public string? SLDLST { get; set; }

        [DataMember(Name = "CountyFIPS ")]
        public string? CountyFIPS { get; set; }

        [DataMember(Name = "CensusBlock ")]
        public string? CensusBlock { get; set; }
        public override string ToString()
        {
            string output = "      AddressComponents:\n";
            output += $"        PremiseNumber                  : {PremiseNumber}\n";
            output += $"        Thoroughfare                   : {Thoroughfare}\n";
            output += $"        DoubleDependentLocality        : {DoubleDependentLocality}\n";
            output += $"        DependentLocality              : {DependentLocality}\n";
            output += $"        Locality                       : {Locality}\n";
            output += $"        AdministrativeArea1            : {AdministrativeArea1}\n";
            output += $"        AdministrativeArea1Abbreviation: {AdministrativeArea1Abbreviation}\n";
            output += $"        AdministrativeArea2            : {AdministrativeArea2}\n";
            output += $"        AdministrativeArea2Abbreviation:{AdministrativeArea2Abbreviation}\n";
            output += $"        AdministrativeArea3            :{AdministrativeArea3}\n";
            output += $"        AdministrativeArea3Abbreviation:{AdministrativeArea3Abbreviation}\n";
            output += $"        AdministrativeArea4            :{AdministrativeArea4}\n";
            output += $"        AdministrativeArea4Abbreviation:{AdministrativeArea4Abbreviation}\n";
            output += $"        PostalCode                     : {PostalCode}\n";
            output += $"        Country                        : {Country}\n";
            output += $"        CountryISO2                    : {CountryISO2}\n";
            output += $"        CountryISO3                    : {CountryISO3}\n";
            output += $"        TimeZone_UTC                   : {TimeZone_UTC}\n";
            output += $"        CongressCode                   : {CongressCode}\n";
            output += $"        GoogleMapsURL                  : {GoogleMapsURL}\n";
            output += $"        PlaceName                      : {PlaceName}\n";
            output += $"        IsUnincorporated               : {IsUnincorporated}\n";
            output += $"        StateFIPS                      : {StateFIPS}\n";
            output += $"        CountyFIPS                     : {CountyFIPS}\n";
            output += $"        CensusTract                    : {CensusTract}\n";
            output += $"        CensusBlock                    : {CensusBlock}\n";
            output += $"        CensusGeoID                    : {CensusGeoID}\n";
            output += $"        ClassFP                        : {ClassFP}\n";
            output += $"        SLDUST                         : {SLDUST}\n";
            output += $"        SLDLST                         : {SLDLST}\n";


            return output;
        }
    }

    /// <summary>
    /// Error block returned if the API fails or receives invalid input.
    /// </summary>
    [DataContract]
    public class ErrorDetails
    {
        [DataMember(Name = "Type")]
        public string? Type { get; set; }

        [DataMember(Name = "TypeCode")]
        public string? TypeCode { get; set; }

        [DataMember(Name = "Desc")]
        public string? Desc { get; set; }

        [DataMember(Name = "DescCode")]
        public string? DescCode { get; set; }

        public override string ToString()
        {
            string output = "  Error:\n";
            output += $"    Type    : {Type}\n";
            output += $"    TypeCode: {TypeCode}\n";
            output += $"    Desc    : {Desc}\n";
            output += $"    DescCode: {DescCode}\n";
            return output;
        }
    }
}

using System;
using System.IO;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using System.Web;

namespace address_geocode_international_dot_net.REST
{
    /// <summary>
    /// Helper class for performing HTTP GET requests with JSON response handling.
    /// </summary>
    public class Helper
    {
        public static T HttpGet<T>(string url, int timeoutSeconds)
        {
            using var httpClient = new HttpClient
            {
                Timeout = TimeSpan.FromSeconds(timeoutSeconds)
            };
            using var request = new HttpRequestMessage(HttpMethod.Get, url);
            using HttpResponseMessage response = httpClient
                .SendAsync(request)
                .GetAwaiter()
                .GetResult();
            response.EnsureSuccessStatusCode();
            using Stream responseStream = response.Content
                .ReadAsStreamAsync()
                .GetAwaiter()
                .GetResult();
            var options = new JsonSerializerOptions
            {
                PropertyNameCaseInsensitive = true
            };
            object? obj = JsonSerializer.Deserialize(responseStream, typeof(T), options);
            T result = (T)obj!;
            return result;
        }

        // Asynchronous HTTP GET and JSON deserialize
        public static async Task<T> HttpGetAsync<T>(string url, int timeoutSeconds)
        {
            HttpClient HttpClient = new HttpClient();
            HttpClient.Timeout = TimeSpan.FromSeconds(timeoutSeconds);
            using var httpResponse = await HttpClient.GetAsync(url).ConfigureAwait(false);
            httpResponse.EnsureSuccessStatusCode();
            var stream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
            return JsonSerializer.Deserialize<T>(stream)!;
        }

        public static string UrlEncode(string value) => HttpUtility.UrlEncode(value ?? string.Empty);
    }
}

Address Geocode – International Python Code Snippet

'''
Service Objects - AGI PlaceSearch Client

This module provides the place_search function to search and geocode international places
using Service Objects' AGI PlaceSearch REST API. It supports trial/live environments,
fallback handling, and extended parameter support.

Functions:
    place_search(single_line: str,
                address1: str,
                address2: str,
                address3: str,
                address4: str,
                address5: str,
                locality: str,
                administrative_area: str,
                postal_code: str,
                country: str,
                boundaries: str,
                max_results: int,
                search_type: str,
                extras: str,
                license_key: str,
                is_live: bool) -> dict:
'''

import requests

# Endpoint URLs for AGI PlaceSearch
PRIMARY_URL = "https://sws.serviceobjects.com/AGI/api.svc/json/PlaceSearch"
BACKUP_URL = "https://swsbackup.serviceobjects.com/AGI/api.svc/PlaceSearch"
TRIAL_URL = "https://trial.serviceobjects.com/AGI/api.svc/json/PlaceSearch"

def place_search(single_line: str,
                address1: str,
                address2: str,
                address3: str,
                address4: str,
                address5: str,
                locality: str,
                administrative_area: str,
                postal_code: str,
                country: str,
                boundaries: str,
                max_results: int,
                search_type: str,
                extras: str,
                license_key: str,
                is_live: bool) -> dict:
    """
    Calls the AGI PlaceSearch API and returns the parsed response.

    Parameters:
        single_line (str): Full address or place name (takes priority).
        address1–5 (str): Optional multiline address input fields.
        locality (str): City or locality.
        administrative_area (str): State, province, or region.
        postal_code (str): ZIP or postal code.
        country (str): ISO-2 country code.
        boundaries (str): Optional boundary filter.
        max_results (int): Number of results to return.
        search_type (str): Type of search: Address, Locality, PostalCode, etc.
        extras (str): Optional additional flags.
        license_key (str): AGI API key.
        is_live (bool): True for live endpoint; False for trial.

    Returns:
        dict: Parsed JSON response from the API.

    Raises:
        RuntimeError: If the API returns an error payload.
        requests.RequestException: On network/HTTP failures (trial mode).
    """

    # Prepare query parameters for AGI API
    params = {
        "SingleLine": single_line,
        "Address1": address1,
        "Address2": address2,
        "Address3": address3,
        "Address4": address4,
        "Address5": address5,
        "Locality": locality,
        "AdministrativeArea": administrative_area,
        "PostalCode": postal_code,
        "Country": country,
        "Boundaries": boundaries,
        "MaxResults": max_results,
        "SearchType": search_type,
        "Extras": extras,
        "LicenseKey": license_key
    }

    # Select the base URL: production vs trial
    url = PRIMARY_URL if is_live else TRIAL_URL

    # Attempt primary (or trial) endpoint first
    try:
        response = requests.get(url, params=params, timeout=10)
        response.raise_for_status()
        data = response.json()

        # If API returned an error in JSON payload, trigger fallback
        error = getattr(response, 'Error', None)
        if not (error is None or getattr(error, 'TypeCode', None) != "3"):
            if is_live:
                # Try backup URL when live
                response = requests.get(BACKUP_URL, params=params, timeout=10)
                data = response.json()

                # If still error, propagate exception
                if "Error" in data:
                    raise RuntimeError(f"AGI PlaceSearch backup error: {data['Error']}")
            else:
                # Trial mode should not fallback; error is terminal
                return data

        # Success: return parsed JSON data
        return data

    except requests.RequestException as req_exc:
        # Network or HTTP-level error occurred
        if is_live:
            try:
                # Fallback to backup URL on network failure
                response = requests.get(BACKUP_URL, params=params, timeout=10)
                response.raise_for_status()
                data = response.json()
                return data
            except Exception as fallback_exc:
                raise RuntimeError("AGI PlaceSearch unreachable on both endpoints") from fallback_exc

        else:
            # In trial mode, propagate the network exception
            raise RuntimeError(f"AGI trial error: {str(req_exc)}") from req_exc

Address Geocode – International NodeJS Code Snippet

import axios from 'axios';
import querystring from 'querystring';
import { PSResponse } from './agi_response.js';

/**
 * @constant
 * @type {string}
 * @description The base URL for the live ServiceObjects Address Geocode International (AGI) API service.
 */
const LiveBaseUrl = 'https://sws.serviceobjects.com/agi/api.svc/json/';

/**
 * @constant
 * @type {string}
 * @description The base URL for the backup ServiceObjects Address Geocode International (AGI) API service.
 */
const BackupBaseUrl = 'https://swsbackup.serviceobjects.com/agi/api.svc/json/';

/**
 * @constant
 * @type {string}
 * @description The base URL for the trial ServiceObjects Address Geocode International (AGI) API service.
 */
const TrialBaseUrl = 'https://trial.serviceobjects.com/agi/api.svc/json/';

/**
 * <summary>
 * Checks if a response from the API is valid by verifying that it either has no Error object
 * or the Error.TypeCode is not equal to '3'.
 * </summary>
 * <param name="response" type="Object">The API response object to validate.</param>
 * <returns type="boolean">True if the response is valid, false otherwise.</returns>
 */
const isValid = (response) => !response?.Error || response.Error.TypeCode !== '3';

/**
 * <summary>
 * Constructs a full URL for the PlaceSearch API endpoint by combining the base URL
 * with query parameters derived from the input parameters.
 * </summary>
 * <param name="params" type="Object">An object containing all the input parameters.</param>
 * <param name="baseUrl" type="string">The base URL for the API service (live, backup, or trial).</param>
 * <returns type="string">The constructed URL with query parameters.</returns>
 */
const buildUrl = (params, baseUrl) =>
    `${baseUrl}PlaceSearch?${querystring.stringify(params)}`;

/**
 * <summary>
 * Performs an HTTP GET request to the specified URL with a given timeout.
 * </summary>
 * <param name="url" type="string">The URL to send the GET request to.</param>
 * <param name="timeoutSeconds" type="number">The timeout duration in seconds for the request.</param>
 * <returns type="Promise<PSResponse>">A promise that resolves to a PSResponse object containing the API response data.</returns>
 * <exception cref="Error">Thrown if the HTTP request fails, with a message detailing the error.</exception>
 */
const httpGet = async (url, timeoutSeconds) => {
    try {
        const response = await axios.get(url, { timeout: timeoutSeconds * 1000 });
        return new PSResponse(response.data);
    } catch (error) {
        throw new Error(`HTTP request failed: ${error.message}`);
    }
};

/**
 * <summary>
 * Provides functionality to call the ServiceObjects Address Geocode International (AGI) API's PlaceSearch endpoint,
 * retrieving geocoded location information for a given address or place with fallback to a backup endpoint for reliability in live mode.
 * </summary>
 */
const PlaceSearchClient = {
    /**
     * <summary>
     * Asynchronously invokes the PlaceSearch API endpoint, attempting the primary endpoint
     * first and falling back to the backup if the response is invalid (Error.TypeCode == '3') in live mode.
     * </summary>
     * @param {string} SingleLine - The full address on one line. Optional; for best results, use parsed inputs.
     * @param {string} Address1 - Address Line 1 of the international address. Optional.
     * @param {string} Address2 - Address Line 2 of the international address. Optional.
     * @param {string} Address3 - Address Line 3 of the international address. Optional.
     * @param {string} Address4 - Address Line 4 of the international address. Optional.
     * @param {string} Address5 - Address Line 5 of the international address. Optional.
     * @param {string} Locality - The name of the locality (e.g., city, town). Optional.
     * @param {string} AdministrativeArea - The administrative area (e.g., state, province). Optional.
     * @param {string} PostalCode - The postal code. Optional.
     * @param {string} Country - The country name or ISO 3166-1 Alpha-2/Alpha-3 code. Required.
     * @param {string} Boundaries - A comma-delimited list of coordinates for search boundaries. Optional; not currently used.
     * @param {string} MaxResults - Maximum number of results (1-10). Defaults to '10'.
     * @param {string} SearchType - Type of search (e.g., 'BestMatch', 'All'). Defaults to 'BestMatch'.
     * @param {string} Extras - Comma-delimited list of extra features. Optional.
     * @param {string} LicenseKey - Your license key to use the service. Required.
     * @param {boolean} isLive - Value to determine whether to use the live or trial servers. Defaults to true.
     * @param {number} timeoutSeconds - Timeout, in seconds, for the call to the service. Defaults to 15.
     * @returns {Promise<PSResponse>} - A promise that resolves to a PSResponse object.
     */
    async invokeAsync(SingleLine, Address1, Address2, Address3, Address4, Address5, Locality, AdministrativeArea, PostalCode, Country, Boundaries, MaxResults = '10', SearchType = 'BestMatch', Extras, LicenseKey, isLive = true, timeoutSeconds = 15) {
        const params = {
            SingleLine,
            Address1,
            Address2,
            Address3,
            Address4,
            Address5,
            Locality,
            AdministrativeArea,
            PostalCode,
            Country,
            Boundaries,
            MaxResults,
            SearchType,
            Extras,
            LicenseKey
        };

        // Remove null/undefined params to avoid empty query params
        //Object.keys(params).forEach(key => params[key] == null && delete params[key]);

        const url = buildUrl(params, isLive ? LiveBaseUrl : TrialBaseUrl);
        let response = await httpGet(url, timeoutSeconds);

        if (isLive && !isValid(response)) {
            const fallbackUrl = buildUrl(params, BackupBaseUrl);
            const fallbackResponse = await httpGet(fallbackUrl, timeoutSeconds);
            return fallbackResponse;
        }
        return response;
    },

    /**
     * <summary>
     * Synchronously invokes the PlaceSearch API endpoint by wrapping the async call
     * and awaiting its result immediately.
     * </summary>
     * @param {string} SingleLine - The full address on one line. Optional; for best results, use parsed inputs.
     * @param {string} Address1 - Address Line 1 of the international address. Optional.
     * @param {string} Address2 - Address Line 2 of the international address. Optional.
     * @param {string} Address3 - Address Line 3 of the international address. Optional.
     * @param {string} Address4 - Address Line 4 of the international address. Optional.
     * @param {string} Address5 - Address Line 5 of the international address. Optional.
     * @param {string} Locality - The name of the locality (e.g., city, town). Optional.
     * @param {string} AdministrativeArea - The administrative area (e.g., state, province). Optional.
     * @param {string} PostalCode - The postal code. Optional.
     * @param {string} Country - The country name or ISO 3166-1 Alpha-2/Alpha-3 code. Required.
     * @param {string} Boundaries - A comma-delimited list of coordinates for search boundaries. Optional; not currently used.
     * @param {string} MaxResults - Maximum number of results (1-10). Defaults to '10'.
     * @param {string} SearchType - Type of search (e.g., 'BestMatch', 'All'). Defaults to 'BestMatch'.
     * @param {string} Extras - Comma-delimited list of extra features. Optional.
     * @param {string} LicenseKey - Your license key to use the service. Required.
     * @param {boolean} isLive - Value to determine whether to use the live or trial servers. Defaults to true.
     * @param {number} timeoutSeconds - Timeout, in seconds, for the call to the service. Defaults to 15.
     * @returns {PSResponse} - A PSResponse object with geocoded location details or an error.
     */
    invoke(SingleLine, Address1, Address2, Address3, Address4, Address5, Locality, AdministrativeArea, PostalCode, Country, Boundaries, MaxResults = '10', SearchType = 'BestMatch', Extras, LicenseKey, isLive = true, timeoutSeconds = 15) {
        return (async () => await this.invokeAsync(
            SingleLine, Address1, Address2, Address3, Address4, Address5, Locality, AdministrativeArea, PostalCode, Country, Boundaries, MaxResults, SearchType, Extras, LicenseKey, isLive, timeoutSeconds
        ))();
    }
};

export { PlaceSearchClient, PSResponse };


export class SearchInfo {
    constructor(data = {}) {
        this.Status = data.Status;
        this.NumberOfLocations = data.NumberOfLocations || 0;
        this.Notes = data.Notes;
        this.NotesDesc = data.NotesDesc;
        this.Warnings = data.Warnings;
        this.WarningDesc = data.WarningDesc;
    }

    toString() {
        return `Status: ${this.Status}, ` +
            `NumberOfLocations: ${this.NumberOfLocations}, ` +
            `Notes: ${this.Notes}, ` +
            `NotesDesc: ${this.NotesDesc}, ` +
            `Warnings: ${this.Warnings}, ` +
            `WarningDesc: ${this.WarningDesc}`;
    }
}

export class AddressComponents {
    constructor(data = {}) {
        this.PremiseNumber = data.PremiseNumber;
        this.Thoroughfare = data.Thoroughfare;
        this.DoubleDependentLocality = data.DoubleDependentLocality;
        this.DependentLocality = data.DependentLocality;
        this.Locality = data.Locality;
        this.AdministrativeArea1 = data.AdministrativeArea1;
        this.AdministrativeArea1Abbreviation = data.AdministrativeArea1Abbreviation;
        this.AdministrativeArea2 = data.AdministrativeArea2;
        this.AdministrativeArea2Abbreviation = data.AdministrativeArea2Abbreviation;
        this.AdministrativeArea3 = data.AdministrativeArea3;
        this.AdministrativeArea3Abbreviation = data.AdministrativeArea3Abbreviation;
        this.AdministrativeArea4 = data.AdministrativeArea4;
        this.AdministrativeArea4Abbreviation = data.AdministrativeArea4Abbreviation;
        this.PostalCode = data.PostalCode;
        this.Country = data.Country;
        this.CountryISO2 = data.CountryISO2;
        this.CountryISO3 = data.CountryISO3;
        this.GoogleMapsURL = data.GoogleMapsURL;
        this.PlaceName = data.PlaceName;
        this.IsUnincorporated = data.IsUnincorporated;
        this.StateFIPS = data.StateFIPS;
        this.CountyFIPS = data.CountyFIPS;
        this.CensusTract = data.CensusTract;
        this.CensusBlock = data.CensusBlock;
        this.CensusGeoID = data.CensusGeoID;
        this.ClassFP = data.ClassFP;
        this.CongressCode = data.CongressCode;
        this.SLDUST = data.SLDUST;
        this.SLDLST = data.SLDLST;
        this.Timezone_UTC = data.Timezone_UTC;
    }

    toString() {
        return `PremiseNumber: ${this.PremiseNumber}, ` +
            `Thoroughfare: ${this.Thoroughfare}, ` +
            `DoubleDependentLocality: ${this.DoubleDependentLocality}, ` +
            `DependentLocality: ${this.DependentLocality}, ` +
            `Locality: ${this.Locality}, ` +
            `AdministrativeArea1: ${this.AdministrativeArea1}, ` +
            `AdministrativeArea1Abbreviation: ${this.AdministrativeArea1Abbreviation}, ` +
            `AdministrativeArea2: ${this.AdministrativeArea2}, ` +
            `AdministrativeArea2Abbreviation: ${this.AdministrativeArea2Abbreviation}, ` +
            `AdministrativeArea3: ${this.AdministrativeArea3}, ` +
            `AdministrativeArea3Abbreviation: ${this.AdministrativeArea3Abbreviation}, ` +
            `AdministrativeArea4: ${this.AdministrativeArea4}, ` +
            `AdministrativeArea4Abbreviation: ${this.AdministrativeArea4Abbreviation}, ` +
            `PostalCode: ${this.PostalCode}, ` +
            `Country: ${this.Country}, ` +
            `CountryISO2: ${this.CountryISO2}, ` +
            `CountryISO3: ${this.CountryISO3}, ` +
            `GoogleMapsURL: ${this.GoogleMapsURL}, ` +
            `PlaceName: ${this.PlaceName}, ` +
            `IsUnincorporated: ${this.IsUnincorporated}, ` +
            `StateFIPS: ${this.StateFIPS}, ` +
            `CountyFIPS: ${this.CountyFIPS}, ` +
            `CensusTract: ${this.CensusTract}, ` +
            `CensusBlock: ${this.CensusBlock}, ` +
            `CensusGeoID: ${this.CensusGeoID}, ` +
            `ClassFP: ${this.ClassFP}, ` +
            `CongressCode: ${this.CongressCode}, ` +
            `SLDUST: ${this.SLDUST}, ` +
            `SLDLST: ${this.SLDLST}, ` +
            `Timezone_UTC: ${this.Timezone_UTC}`;
    }
}
export class LocationInfo {
    constructor(data = {}) {
        this.PrecisionLevel = data.PrecisionLevel || 0;
        this.Type = data.Type;
        this.Latitude = data.Latitude;
        this.Longitude = data.Longitude;
        this.AddressComponents = data.AddressComponents ? new AddressComponents(data.AddressComponents) : null;
    }

    toString() {
        return `PrecisionLevel: ${this.PrecisionLevel}, ` +
            `Type: ${this.Type}, ` +
            `Latitude: ${this.Latitude}, ` +
            `Longitude: ${this.Longitude}, ` +
            `AddressComponents: ${this.AddressComponents ? this.AddressComponents.toString() : 'null'}`;
    }
}
export class Error {
    constructor(data = {}) {
        this.Type = data.Type;
        this.TypeCode = data.TypeCode;
        this.Desc = data.Desc;
        this.DescCode = data.DescCode;
    }

    toString() {
        return `Type: ${this.Type}, TypeCode: ${this.TypeCode}, Desc: ${this.Desc}, DescCode: ${this.DescCode}`;
    }
}

export class PSResponse {
    constructor(data = {}) {
        this.SearchInfo = data.SearchInfo ? new SearchInfo(data.SearchInfo) : null;
        this.Locations = (data.Locations || []).map(location => new LocationInfo(location));
        this.Error = data.Error ? new Error(data.Error) : null;
    }

    toString() {
        const locationsString = this.Locations.length
            ? this.Locations.map(location => location.toString()).join(', ')
            : 'None';
        return `PSResponse: SearchInfo = ${this.SearchInfo ? this.SearchInfo.toString() : 'null'}, ` +
            `Locations = [${locationsString}], ` +
            `Error = ${this.Error ? this.Error.toString() : 'null'}`;
    }
}
export class RSResponse {
    constructor(data = {}) {
        this.SearchInfo = data.SearchInfo ? new SearchInfo(data.SearchInfo) : null;
        this.Locations = (data.Locations || []).map(location => new LocationInfo(location));
        this.Error = data.Error ? new Error(data.Error) : null;
    }

    toString() {
        let result = 'RSResponse:\n';
        if (this.SearchInfo) {
            result += `SearchInfo: ${this.SearchInfo.toString()}\n`;
        }
        if (this.Locations && this.Locations.length > 0) {
            result += 'Locations:\n';
            result += this.Locations.map(location => location.toString()).join('\n') + '\n';
        }
        if (this.Error) {
            result += `Error: ${this.Error.toString()}`;
        }
        return result;
    }
}

export default { PSResponse, RSResponse };