Private/Get-TripwireDistanceKm.ps1

<#
.SYNOPSIS
    Great-circle distance between two coordinates, in kilometres.

.DESCRIPTION
    Used only by the impossible-travel component of the origin signal. Haversine is
    deliberate: it under-estimates the real path a traveller must fly, so any speed it
    derives is a LOWER bound. An impossible-travel verdict built on a lower bound cannot
    be an artefact of the distance model -- if this says 4,000 km/h, the real figure is
    at least that.

    Returns $null when either coordinate pair is missing, so the caller can withhold the
    signal instead of inventing one. Graph omits location on sign-ins from private ranges
    and from some non-interactive flows, and "no location" must never read as "same city".
#>

function Get-TripwireDistanceKm {
    [CmdletBinding()]
    [OutputType([double])]
    param(
        [AllowNull()] [System.Nullable[double]] $Lat1,
        [AllowNull()] [System.Nullable[double]] $Lon1,
        [AllowNull()] [System.Nullable[double]] $Lat2,
        [AllowNull()] [System.Nullable[double]] $Lon2
    )

    if ($null -eq $Lat1 -or $null -eq $Lon1 -or $null -eq $Lat2 -or $null -eq $Lon2) { return $null }

    $earthRadiusKm = 6371.0088
    $toRad = [Math]::PI / 180.0

    $dLat = ([double]$Lat2 - [double]$Lat1) * $toRad
    $dLon = ([double]$Lon2 - [double]$Lon1) * $toRad

    $a = [Math]::Sin($dLat / 2) * [Math]::Sin($dLat / 2) +
         [Math]::Cos([double]$Lat1 * $toRad) * [Math]::Cos([double]$Lat2 * $toRad) *
         [Math]::Sin($dLon / 2) * [Math]::Sin($dLon / 2)

    $c = 2 * [Math]::Atan2([Math]::Sqrt($a), [Math]::Sqrt(1 - $a))
    return [Math]::Round($earthRadiusKm * $c, 3)
}