OpenCage.psm1
Set-StrictMode -Version Latest $script:ModuleVersion = '0.2.0' $script:ModuleUserAgent = "OpenCage.PowerShell/$($script:ModuleVersion) (+https://github.com/aliragas/opencage-powershell)" $script:InvariantCulture = [System.Globalization.CultureInfo]::InvariantCulture function Get-OpenCageApiKey { [CmdletBinding()] param( [string]$ApiKey ) if (-not [string]::IsNullOrWhiteSpace($ApiKey)) { return $ApiKey } $scopes = @('Process', 'User', 'Machine') foreach ($scope in $scopes) { $value = [Environment]::GetEnvironmentVariable('OPENCAGE_API_KEY', $scope) if (-not [string]::IsNullOrWhiteSpace($value)) { return $value } } throw [System.InvalidOperationException]::new('OpenCage API key not found. Set the OPENCAGE_API_KEY environment variable or use the -ApiKey parameter.') } function ConvertTo-OpenCageQueryString { [CmdletBinding()] param( [Parameter(Mandatory)][hashtable]$Parameters ) $pairs = [System.Collections.Generic.List[string]]::new() foreach ($key in $Parameters.Keys) { $value = $Parameters[$key] if ($null -eq $value) { continue } $escapedKey = [Uri]::EscapeDataString($key) switch ($value) { { $_ -is [bool] } { $escapedValue = [Uri]::EscapeDataString(([Convert]::ToInt32($_)).ToString()) $pairs.Add("$escapedKey=$escapedValue") continue } { $_ -is [System.Array] -and -not ($_ -is [string]) } { $stringValue = ($_ | ForEach-Object { if ($_ -is [System.IFormattable]) { $_.ToString($null, $script:InvariantCulture) } else { $_.ToString() } }) -join ',' $escapedValue = [Uri]::EscapeDataString($stringValue) $pairs.Add("$escapedKey=$escapedValue") continue } default { if ($value -is [System.IFormattable]) { $stringValue = $value.ToString($null, $script:InvariantCulture) } else { $stringValue = $value.ToString() } $escapedValue = [Uri]::EscapeDataString($stringValue) $pairs.Add("$escapedKey=$escapedValue") } } } return ($pairs -join '&') } function Invoke-OpenCageApiRequest { [CmdletBinding()] param( [Parameter(Mandatory)][hashtable]$Parameters, [string]$ApiKey ) $resolvedKey = Get-OpenCageApiKey -ApiKey $ApiKey if (-not $Parameters.ContainsKey('q') -or [string]::IsNullOrWhiteSpace($Parameters['q'])) { throw [System.ArgumentException]::new('Query parameter "q" is required and cannot be empty.') } $effectiveParameters = [ordered]@{} foreach ($existingKey in $Parameters.Keys) { $effectiveParameters[$existingKey] = $Parameters[$existingKey] } $effectiveParameters['key'] = $resolvedKey $queryString = ConvertTo-OpenCageQueryString -Parameters $effectiveParameters $uri = "https://api.opencagedata.com/geocode/v1/json?$queryString" $headers = @{ 'User-Agent' = $script:ModuleUserAgent 'Accept' = 'application/json' } $responseHeaders = $null $statusCodeValue = $null $responseBody = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers -SkipHttpErrorCheck -StatusCodeVariable statusCodeValue -ResponseHeadersVariable responseHeaders -ErrorAction Stop if ($null -eq $responseBody) { throw [System.Exception]::new('The OpenCage API did not return a response body.') } if (-not ($responseBody.PSObject.Properties.Name -contains 'status')) { throw [System.Exception]::new('Unexpected OpenCage API response format: missing status information.') } $statusCode = $responseBody.status.code $statusMessage = $responseBody.status.message if ($statusCode -ne 200) { $errorMessage = if ($statusMessage) { "OpenCage API error $($statusCode): $statusMessage" } else { "OpenCage API error $($statusCode)" } if ($statusCode -in @(402, 403)) { throw [System.InvalidOperationException]::new($errorMessage) } throw [System.Exception]::new($errorMessage) } $results = $responseBody.results if ($null -eq $results) { $resultCollection = @() } elseif ($results -is [System.Collections.IEnumerable] -and -not ($results -is [string])) { $resultCollection = @($results) } else { $resultCollection = ,$results } $hasResults = ($resultCollection.Count -gt 0) $rate = $null if ($responseBody.PSObject.Properties.Name -contains 'rate') { $rate = $responseBody.rate } $httpStatusCode = $null if ($null -ne $statusCodeValue) { try { $httpStatusCode = [int]$statusCodeValue } catch { $httpStatusCode = $statusCodeValue } } return [pscustomobject]@{ Query = $effectiveParameters['q'] RequestUri = $uri HttpStatusCode = $httpStatusCode Status = $responseBody.status TotalResults = $responseBody.total_results HasResults = $hasResults Results = $resultCollection Rate = $rate ResponseHeaders = $responseHeaders Raw = $responseBody } } function Add-OpenCageOptionalParameters { param( [ref]$Target, [hashtable]$Source ) if ($null -eq $Source) { return } $dictionary = $Target.Value foreach ($key in $Source.Keys) { if ([string]::IsNullOrWhiteSpace($key)) { continue } if (@('q', 'key') -contains $key) { continue } $value = $Source[$key] if ($null -eq $value) { continue } if ($value -is [bool]) { $dictionary[$key] = if ($value) { 1 } else { 0 } } elseif ($value -is [System.Management.Automation.SwitchParameter]) { $dictionary[$key] = if ($value.IsPresent) { 1 } else { 0 } } elseif ($value -is [System.IFormattable]) { $dictionary[$key] = $value.ToString($null, $script:InvariantCulture) } else { $dictionary[$key] = $value } } $Target.Value = $dictionary } <# .SYNOPSIS Performs forward geocoding via the OpenCage Geocoding API. .DESCRIPTION Sends an address or placename query to the OpenCage API and returns a structured response containing metadata, results, HTTP status, headers, and the original request URI. Supports the most common optional query parameters, defensive parsing for missing fields, and a -Raw switch for accessing the unmodified API payload. .PARAMETER Query A forward geocoding query string (address or placename). Must be at least two characters once trimmed. .PARAMETER ApiKey Overrides the OPENCAGE_API_KEY environment variable. If omitted the environment variable must be set. .PARAMETER CountryCode One or more ISO 3166-1 alpha-2 country codes used to restrict results. Codes are normalized to lowercase as required by the API. .PARAMETER Language Preferred language for the response (IETF language tag such as "en" or "pt-BR"). .PARAMETER Limit Maximum number of forward geocoding results to return (1-100). .PARAMETER Bounds Four numeric values describing the southwest and northeast corners of a bounding box in the form: minLongitude, minLatitude, maxLongitude, maxLatitude. .PARAMETER ProximityLatitude Latitude component (decimal degrees) for the proximity bias hint. Must be used together with -ProximityLongitude. .PARAMETER ProximityLongitude Longitude component (decimal degrees) for the proximity bias hint. Must be used together with -ProximityLatitude. .PARAMETER Abbreviate When present, sets the abbrv optional parameter to 1 to request abbreviated formatted strings. .PARAMETER AddressOnly When set, instructs the API to return formatted strings without POI names. .PARAMETER NoAnnotations When set, requests that annotation data be omitted. .PARAMETER NoDedupe Disables result deduplication. .PARAMETER NoRecord Requests the API not to store the query contents. .PARAMETER Pretty Requests a pretty-printed JSON response (for debugging). .PARAMETER RoadInfo Requests the roadinfo optional behavior/annotation. .PARAMETER AdditionalParameters Hashtable of additional optional parameters to include. Reserved keys (q, key) are ignored. .PARAMETER Raw Returns the API payload exactly as received instead of the structured PowerShell response object. .OUTPUTS System.Management.Automation.PSCustomObject .EXAMPLE Invoke-Geocode -Query 'Frauenplan 1, Weimar, Germany' -Limit 1 Retrieves a single result for Goethe National Museum and returns the structured response including metadata such as HttpStatusCode and RequestUri. .EXAMPLE Invoke-Geocode -Query 'Nowhere-Interesting' -Limit 1 Demonstrates handling of the no-results scenario. Inspect the HasResults property to determine whether any matches were found before accessing the Results collection. #> function Invoke-Geocode { [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)][string]$Query, [string]$ApiKey, [string[]]$CountryCode, [string]$Language, [ValidateRange(1,100)][int]$Limit, [double[]]$Bounds, [double]$ProximityLatitude, [double]$ProximityLongitude, [switch]$Abbreviate, [switch]$AddressOnly, [switch]$NoAnnotations, [switch]$NoDedupe, [switch]$NoRecord, [switch]$Pretty, [switch]$RoadInfo, [hashtable]$AdditionalParameters, [switch]$Raw ) $params = [ordered]@{ q = $Query } if ($CountryCode) { $normalizedCodes = @() foreach ($code in $CountryCode) { if ([string]::IsNullOrWhiteSpace($code)) { continue } $normalizedCodes += $code.ToString().ToLowerInvariant() } if (-not $normalizedCodes) { throw [System.ArgumentException]::new('CountryCode must contain at least one non-empty ISO 3166-1 alpha-2 code.') } $params['countrycode'] = [string]::Join(',', $normalizedCodes) } if ($PSBoundParameters.ContainsKey('Language')) { $params['language'] = $Language } if ($PSBoundParameters.ContainsKey('Limit')) { $params['limit'] = $Limit } if ($Bounds) { if ($Bounds.Count -ne 4) { throw [System.ArgumentException]::new('Bounds must contain exactly four numeric values: minLongitude, minLatitude, maxLongitude, maxLatitude.') } $params['bounds'] = ($Bounds | ForEach-Object { $_.ToString($script:InvariantCulture) }) -join ',' } if ($PSBoundParameters.ContainsKey('ProximityLatitude') -or $PSBoundParameters.ContainsKey('ProximityLongitude')) { if (-not ($PSBoundParameters.ContainsKey('ProximityLatitude') -and $PSBoundParameters.ContainsKey('ProximityLongitude'))) { throw [System.ArgumentException]::new('Both ProximityLatitude and ProximityLongitude must be specified together.') } $lat = $ProximityLatitude.ToString($script:InvariantCulture) $lng = $ProximityLongitude.ToString($script:InvariantCulture) $params['proximity'] = "$lat,$lng" } if ($Abbreviate.IsPresent) { $params['abbrv'] = 1 } if ($AddressOnly.IsPresent) { $params['address_only'] = 1 } if ($NoAnnotations.IsPresent) { $params['no_annotations'] = 1 } if ($NoDedupe.IsPresent) { $params['no_dedupe'] = 1 } if ($NoRecord.IsPresent) { $params['no_record'] = 1 } if ($Pretty.IsPresent) { $params['pretty'] = 1 } if ($RoadInfo.IsPresent) { $params['roadinfo'] = 1 } Add-OpenCageOptionalParameters -Target ([ref]$params) -Source $AdditionalParameters $result = Invoke-OpenCageApiRequest -Parameters $params -ApiKey $ApiKey if ($Raw.IsPresent) { return $result.Raw } return $result } <# .SYNOPSIS Performs reverse geocoding via the OpenCage Geocoding API. .DESCRIPTION Sends a latitude/longitude coordinate pair to the OpenCage API and returns a structured response containing metadata, results, HTTP status, headers, and the original request URI. Optional switches mirror the forward geocoding command and a -Raw switch returns the unmodified API payload. .PARAMETER Latitude Decimal degree latitude value between -90 and 90 inclusive. .PARAMETER Longitude Decimal degree longitude value between -180 and 180 inclusive. .PARAMETER ApiKey Overrides the OPENCAGE_API_KEY environment variable. If omitted the environment variable must be set. .PARAMETER Language Preferred response language (IETF language tag such as "en" or "pt-BR"). .PARAMETER AddressOnly When set, instructs the API to return formatted strings without POI names. .PARAMETER NoAnnotations Requests that annotation data be omitted from the response. .PARAMETER NoRecord Requests that the API not store the query contents. .PARAMETER Pretty Requests a pretty-printed JSON response (for debugging). .PARAMETER RoadInfo Requests the roadinfo optional behavior/annotation. .PARAMETER AdditionalParameters Hashtable of additional optional parameters to include. Reserved keys (q, key) are ignored. .PARAMETER Raw Returns the API payload exactly as received instead of the structured PowerShell response object. .OUTPUTS System.Management.Automation.PSCustomObject .EXAMPLE Invoke-ReverseGeocode -Latitude 51.9526622 -Longitude 7.6324709 Returns the address information associated with the supplied coordinates in Münster, Germany. .EXAMPLE Invoke-ReverseGeocode -Latitude 52.5432379 -Longitude 13.4142133 -RoadInfo Requests additional road metadata (when available) while returning the structured response object for further inspection or formatting. #> function Invoke-ReverseGeocode { [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)][ValidateRange(-90,90)][double]$Latitude, [Parameter(Mandatory)][ValidateRange(-180,180)][double]$Longitude, [string]$ApiKey, [string]$Language, [switch]$AddressOnly, [switch]$NoAnnotations, [switch]$NoRecord, [switch]$Pretty, [switch]$RoadInfo, [hashtable]$AdditionalParameters, [switch]$Raw ) $lat = $Latitude.ToString($script:InvariantCulture) $lng = $Longitude.ToString($script:InvariantCulture) $params = [ordered]@{ q = "$lat,$lng" } if ($PSBoundParameters.ContainsKey('Language')) { $params['language'] = $Language } if ($AddressOnly.IsPresent) { $params['address_only'] = 1 } if ($NoAnnotations.IsPresent) { $params['no_annotations'] = 1 } if ($NoRecord.IsPresent) { $params['no_record'] = 1 } if ($Pretty.IsPresent) { $params['pretty'] = 1 } if ($RoadInfo.IsPresent) { $params['roadinfo'] = 1 } Add-OpenCageOptionalParameters -Target ([ref]$params) -Source $AdditionalParameters $result = Invoke-OpenCageApiRequest -Parameters $params -ApiKey $ApiKey if ($Raw.IsPresent) { return $result.Raw } return $result } Export-ModuleMember -Function @('Invoke-Geocode', 'Invoke-ReverseGeocode') |