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.
 
    .PARAMETER Address
        The address to geocode (should include street, city, postalcode, countrycodes)
 
    .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 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()]

    param (

        # Input parameter
         [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position=0)][PSCustomObject]$Address  # the address to geocode (should include street, city, postalcode, countrycodes)
        ,[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

        # 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
        $start = [datetime]::Now # fill this variable

        $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
        }


        #-----------------------------------------------
        # 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
        $hashedInput = Get-AddressHash -Address $Address


        #-----------------------------------------------
        # 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 } | 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 )search")
            $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 ( $i -gt 0 ) {
                $ts = New-TimeSpan -Start $start -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
            $start = [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 {
        Write-Verbose "Geocoded $( $i ) addresses"
    }

}