public/OSM/Invoke-OSM.ps1
<#
.SYNOPSIS Wrapper for OpenStreetMaps to input address data and get back geocoded and fixed addresses and maybe some more information. .DESCRIPTION Apteco PS Modules - PowerShell OSM Geocoding Just define an address like $addr = [PSCustomObject]@{ "street" = "Schaumainkai 87" "city" = "Frankfurt" "postalcode" = 60589 "countrycodes" = "de" } and geocode it $addr | Invoke-OSM -Email "user@example.com" -AddressDetails -ExtraTags -ResultsLanguage "de" If you put in multiple objects, the geocoding will do 1 request per second like it should do to cover OSM terms and conditions. To reverse geocode, do something like Invoke-OSM -Lat 50.1011058 -Lon 8.6696359 -Email "user@example.com" -AddressDetails -ExtraTags -ResultsLanguage "de" Pipeline support is at the moment only for address search geocoding, not reverse geocoding .PARAMETER Address The address to geocode (should include street, city, postalcode, countrycodes) .PARAMETER Lat To reverse geocode, you could just insert lat and lon .PARAMETER Lon To reverse geocode, you could just insert lat and lon .PARAMETER Id To allow using an id and saving it, there is an optional parameter for that .PARAMETER Email The email is a kind of useragent for identification for the current process .PARAMETER ResultsLanguage Language for the results .PARAMETER ExcludeKnownHashes This parameter leads to exclude hashes that are already in the cache Be aware, that this parameter kills known records that come in so if your input is a combination of id and address, this object won't be forwarded for known addresses .PARAMETER CombineIdAndHash Combine ID and address hash value so you definitely have every ID of your input data, even if hashed addresses are the same. This is useful when you later join OSM geocodes via an ID rather than a hashed address. Only works, if the inputobject has a property with the name ID or Id or id. .PARAMETER AddressDetails Load more details from OSM .PARAMETER ExtraTags Load extra tags from OSM .PARAMETER NameDetails Load name details from OSM like opening hours etc. .PARAMETER ReturnOnlyFirstPosition If there are multiple addresses in the result, return only the entry at position 1 .PARAMETER AddMetaData Wraps the result with more metadata .PARAMETER AddToHashCache Directly puts the new hash value into the cache so it can be used to exclude some records .PARAMETER ReturnHashTable Instead of PSCustomObject, only works together with -AddMetaData .PARAMETER ReturnJson Formats the returned addresses as json rather than PSCustomObjects, only works together with -AddMetaData .PARAMETER Verbose Shows you more information about the current status .EXAMPLE $addr = [PSCustomObject]@{"street" = "Schaumainkai 87";"city" = "Frankfurt";"postalcode" = 60589;"countrycodes" = "de"} $addr | Invoke-OSM -Email "user@example.com" -AddressDetails -ExtraTags -ResultsLanguage "de" .INPUTS Objects .OUTPUTS Objects .NOTES Author: florian.von.bracht@apteco.de #> function Invoke-OSM { [CmdletBinding(DefaultParameterSetName='search')] param ( # Input parameter [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position=0, ParameterSetName="search")][PSCustomObject]$Address # the address to geocode (should include street, city, postalcode, countrycodes) ,[Parameter(Mandatory = $true, ValueFromPipeline = $false, Position=0, ParameterSetName="reverse")][Double]$Lat ,[Parameter(Mandatory = $true, ValueFromPipeline = $false, Position=1, ParameterSetName="reverse")][Double]$Lon ,[Parameter(Mandatory = $false, ValueFromPipeline = $false, Position=2, ParameterSetName="reverse")][String]$Id = "" # To allow using an id and saving it, there is an optional parameter for that ,[Parameter(Mandatory = $true)][String]$Email # the email is a kind of useragent for identification for the current process ,[Parameter(Mandatory = $false)][String]$ResultsLanguage = "de" # language for the results ,[Parameter(Mandatory = $false)][Switch]$ExcludeKnownHashes = $false # this parameter leads to exclude hashes that are already in the cache # Be aware, that this parameter kills known records that come in # so if your input is a combination of id and address, this object won't # be forwarded for known addresses ,[Parameter(Mandatory = $false)][Switch]$CombineIdAndHash = $false # Combine ID and address hash value so you definitely have every # ID of your input data, even if hashed addresses are the same # This is useful when you later join OSM geocodes via an ID rather # than a hashed address # More OSM data ,[Parameter(Mandatory = $false)][Switch]$AddressDetails = $false # load more details from osm ,[Parameter(Mandatory = $false)][Switch]$ExtraTags = $false # load extra tags from osm ,[Parameter(Mandatory = $false)][Switch]$NameDetails = $false # load name details from osm like opening hours etc. # Options for return ,[Parameter(Mandatory = $false)][Switch]$ReturnOnlyFirstPosition = $false # if there are multiple addresses in the result, return only the entry at position 1 ,[Parameter(Mandatory = $false)][Switch]$AddMetaData = $false # wraps the result with more metadata ,[Parameter(Mandatory = $false)][Switch]$AddToHashCache = $false # Directly puts the new hash value into the cache so it can be used to exclude some records ,[Parameter(Mandatory = $false)][Switch]$ReturnHashTable = $false # Instead of PSCustomObject, only works together with -AddMetaData ,[Parameter(Mandatory = $false)][Switch]$ReturnJson = $false # Formats the returned addresses as json rather than PSCustomObjects, only works together with -AddMetaData ) begin { #----------------------------------------------- # START #----------------------------------------------- #Add-Type -AssemblyName System.Web # outcomment later #Import-Module ConvertStrings $i = 0 $maxMillisecondsPerRequest = 1000 #$settings.millisecondsPerRequest #Write-Log "Will create 1 request per $( $maxMillisecondsPerRequest ) milliseconds" -Severity VERBOSE $base = "https://nominatim.openstreetmap.org/" # TODO put this into settings If ( $AddressDetails -eq $true ) { $loadAddressDetails = 1 } else { $loadAddressDetails = 0 } If ( $ExtraTags -eq $true ) { $loadExtraTags = 1 } else { $loadExtraTags = 0 } If ( $NameDetails -eq $true ) { $loadNameDetails = 1 } else { $loadNameDetails = 0 } $paramSet = $PSCmdlet.ParameterSetName # is search or reverse #----------------------------------------------- # CHECK THE STATUS OF OSM SERVER AT FIRST CALL OF MODULE #----------------------------------------------- If ( $null -eq $Script:statusOk ) { $osmStatus = Invoke-RestMethod -uri "$( $base )status?format=json" -Method GET <# status : 0 message : OK data_updated : 2023-11-16T08:59:45+00:00 software_version : 4.3.0-0 database_version : 4.3.0-0 #> Write-Verbose "OSM Server status: $( $osmStatus.message )" If ( $osmStatus.message -ne "OK" ) { throw "There is a problem with OSM server status" } $Script:statusOk = $osmStatus.message } #----------------------------------------------- # VALIDATE EMAIL #----------------------------------------------- # This throws an exception, if it is not able to parse it $emailAddress = [mailaddress]$Email #----------------------------------------------- # ADDITIONAL HEADERS #----------------------------------------------- # Add additional headers from the settings, e.g. for api gateways or proxies # $Script:settings.additionalHeaders.PSObject.Properties | ForEach-Object { # $updatedParameters.add($_.Name, $_.Value) # } #----------------------------------------------- # CONTENT TYPE #----------------------------------------------- # Set content type, if not present yet # If ( $updatedParameters.ContainsKey("ContentType") -eq $false) { # $updatedParameters.add("ContentType",$Script:settings.contentType) # } } process { #----------------------------------------------- # BUILD THE HASH OF ADDRESS #----------------------------------------------- # Build hash value Switch ( $paramSet ) { "search" { $hashValue = Get-AddressHash -Address $Address -ParameterSetName "search" } "reverse" { $decimalSeparator = (Get-Culture).NumberFormat.NumberDecimalSeparator $Address = [PSCustomObject]@{ "id" = $Id "lat" = $Lat.toString().replace($decimalSeparator, ".") "lon" = $Lon.toString().replace($decimalSeparator, ".") } $hashValue = Get-AddressHash -Address $Address -ParameterSetName "reverse" } } If ( $CombineIdAndHash -eq $true ) { # TODO check case sensitivity $hashedInput = "$( $Address.id )#$( $hashValue )" } else { $hashedInput = $hashValue } #----------------------------------------------- # CHECK IF THIS REQUEST SHOULD BE DONE #----------------------------------------------- If ( $ExcludeKnownHashes -eq $false -or ( $ExcludeKnownHashes -eq $true -and $Script:knownHashes -notcontains $hashedInput )) { #----------------------------------------------- # PREPARE QUERY #----------------------------------------------- $nvCollection = [System.Web.HttpUtility]::ParseQueryString([String]::Empty) #, [System.Text.Encoding]::UTF8) $Address.PSObject.Properties | where-object { $_.Name -in $Script:allowedQueryParameters.$paramSet } | ForEach-Object { $nvCollection.Add( $_.Name, $_.Value ) } # Create address parameter string like streetSchaumainkai%2087&city=Frankfurt&postalcode=60589&countrycodes=de # $addrParams = [System.Collections.ArrayList]@() # $paramMap.Keys | ForEach { # $key = $_ # $value = $addr[$paramMap[$key]] # [void]$addrParams.add("$( $key )=$( [uri]::EscapeDataString($value) )") # } #----------------------------------------------- # ADD MORE TO QUERY #----------------------------------------------- # refers to: https://nominatim.org/release-docs/latest/api/Search/ $nvCollection.Add( "format", "jsonv2" ) $nvCollection.Add( "layer", "address" ) #$nvCollection.Add( "featureType", "city" ) $nvCollection.Add( "dedupe", "1" ) $nvCollection.Add( "debug", "0" ) $nvCollection.Add( "accept-language", $ResultsLanguage ) $nvCollection.Add( "addressdetails", $loadAddressDetails ) $nvCollection.Add( "extratags", $loadExtraTags ) $nvCollection.Add( "namedetails", $loadNameDetails ) $nvCollection.Add( "email", $emailAddress.Address ) #----------------------------------------------- # PREPARE URL #----------------------------------------------- $uriRequest = [System.UriBuilder]::new("$( $base )$( $paramSet )") $uriRequest.Query = [System.Web.HttpUtility]::UrlDecode( $nvCollection.ToString() ) # Using an alternative way becaue umlauts can create massive problems in queries # $queryArray = [Array]@() # $nvCollection.GetEnumerator() | ForEach-Object { # $key = $_ # $queryArray += "$( $key )=$( [uri]::EscapeDataString( $nvCollection[$key] ) )" # } # $uriRequest.Query = $queryArray -join "&" #----------------------------------------------- # LOOP THROUGH DATA #----------------------------------------------- # Parameters for call $restParams = @{ "Uri" = $uriRequest.Uri.OriginalString "Method" = "GET" "UserAgent" = $emailAddress.Address #$script:settings.useragent "ContentType" = "application/json; charset=utf-8" #Verbose = $false } # Wait until 1 second is full, then proceed # This is only relevant for all calls after the first one If ( $null -ne $script:lastCallTimestamp ) { $ts = New-TimeSpan -Start $script:lastCallTimestamp -End ( [datetime]::Now ) if ( $ts.TotalMilliseconds -lt $maxMillisecondsPerRequest ) { $waitLonger = [math]::ceiling( $maxMillisecondsPerRequest - $ts.TotalMilliseconds ) Write-Verbose "Waiting $( $waitLonger ) ms" Start-Sleep -Milliseconds $waitLonger } } Write-Verbose $uriRequest.Uri.OriginalString # Request to OSM $Script:lastCallTimestamp = [datetime]::Now #$t = Measure-Command { # TODO [ ] possibly implement proxy, if needed # TODO add try catch here $res = Invoke-RestMethod @restParams #-Uri $uriRequest.Uri.OriginalString #} $i += 1 #$pl = ConvertTo-Json -InputObject $res -Depth 99 -Compress #----------------------------------------------- # DECIDE TO RETURN WHOLE RESULT OR FIRST ENTRY #----------------------------------------------- If ( $ReturnOnlyFirstPosition -eq $true ) { $ret = $res[0] } else { $ret = $res } #----------------------------------------------- # CACHE HASHVALUE #----------------------------------------------- If ( $AddToHashCache -eq $true ) { Add-ToHashCache -InputHash $hashedInput } #----------------------------------------------- # RETURN RAW OR ADD SOME METADATA #----------------------------------------------- If ( $AddMetaData -eq $true ) { If ( $ReturnJson -eq $true ) { $returnAddress = ConvertTo-Json -InputObject $Address -Depth 99 $returnResults = ConvertTo-Json -InputObject $ret -Depth 99 } else { $returnAddress = $Address $returnResults = $ret } If ( $ReturnHashTable -eq $true ) { [Hashtable]@{ "inputHash" = $hashedInput "inputObject" = $returnAddress "results" = $returnResults "total" = $res.count } } else { [PSCustomObject]@{ "inputHash" = $hashedInput "inputObject" = $returnAddress "results" = $returnResults "total" = $res.count } } } else { $ret } } } end { Switch ( $paramSet ) { "search" { Write-Verbose "Geocoded $( $i ) addresses" } "reverse" { Write-Verbose "Geocoded $( $i ) coordinates" } } } } |