Functions/Public/Find-CtsStop.ps1

function Find-CtsStop {
  <#
  .SYNOPSIS
  Finds CTS stops and lines matching filters
  .DESCRIPTION
  Queries the cached CTS network data and returns [Stop] objects whose name, line and destination match the given
  filters. Stop, line and destination names are matched by prefix (case-insensitive), so partial names work.
  .EXAMPLE
  Find-CtsStop Gallia Gare, Neuhof, Wolfisheim
  Returns all stops named 'Gallia...' with a line heading towards 'Gare...', 'Neuhof...' or 'Wolfisheim...'
  .EXAMPLE
  Find-CtsStop -Line A, D -Destination Kehl, Illkirch -Strict
  Returns all stops for lines 'A' and 'D' with the only destinations 'Kehl...' and 'Illkirch...'
  Destinations with the same direction are excluded: 'Port du Rhin', 'Les Halles'
  .OUTPUTS
  [Stop] objects with the relevant lines and destinations
  #>

  [CmdletBinding(SupportsShouldProcess)]
  [OutputType([Stop])]
  param (
    # Line names to filter by
    [Parameter()]
    [ArgumentCompleter([LineCompleter])]
    [AllowEmptyCollection()]
    [String[]] $Line,

    # Stop names to filter by (prefix match)
    [Parameter(Position = 0)]
    [ArgumentCompleter([StopCompleter])]
    [AllowEmptyCollection()]
    [Alias('From')]
    [String[]] $Stop,

    # Destination names to filter by (prefix match)
    [Parameter(Position = 1)]
    [ArgumentCompleter([DestinationCompleter])]
    [AllowEmptyCollection()]
    [Alias('To')]
    [String[]] $Destination,

    # Only return destinations that exactly match -Destination, excluding same-direction alternatives
    [Parameter()]
    [Switch] $Strict,

    # Enable loose prefix matching for argument completion (internal)
    [Parameter(DontShow)]
    [Switch] $Completion,

    # Bypass the stop cache and fetch fresh data from the API
    [Parameter(DontShow)]
    [Switch] $Force,

    # Skip writing the cache file to disk
    [Parameter(DontShow)]
    [Switch] $NoCacheFile
  )
  process {
    try {
      Update-CtsStopCache -Force:$Force -NoCacheFile:$NoCacheFile
    } catch {
      $PSCmdlet.ThrowTerminatingError($_)
    }

    $StringComparison = [System.StringComparison]::CurrentCultureIgnoreCase
    $Destinations = [StopCache]::Instance.Destinations.GetEnumerator() | Where-Object {
      $LineName = $_.Key
      # Loose match only for argument completion
      if ($Completion) {
        $Line.Count -eq 0 -or $Line.Where({ $LineName.StartsWith($_, $StringComparison) }).Count -gt 0
      } else {
        $Line.Count -eq 0 -or $Line -contains $LineName
      }
    } | ForEach-Object {
      $Dest = $_.Value.GetEnumerator() | Where-Object {
        $DestName = $_.Name
        $Destination.Count -eq 0 -or $Destination.Where({ $DestName.StartsWith($_, $StringComparison) }).Count -gt 0
      }
      if ($Strict) {
        $Dest
      } else {
        # Include same-direction destinations to support CTS network changes
        $Directions = $Dest.Direction | Select-Object -Unique
        $_.Value.GetEnumerator() | Where-Object { $_.Direction -in $Directions }
      }
    }
    $Stops = $Destinations.Stops | Select-Object -Unique | ForEach-Object {
      [StopCache]::Instance.Stops[$_]
    } | Where-Object {
      $StopName = $_.Name
      $Stop.Count -eq 0 -or $Stop.Where({ $StopName.StartsWith($_, $StringComparison) }).Count -gt 0
    }

    $Stops | ForEach-Object {
      $StopId = $_.Id
      $Lines = $Destinations | Where-Object {
        $_.Stops.Contains($StopId)
      } | Group-Object -Property Line | ForEach-Object {
        [Line]@{
          LineInfo     = [StopCache]::Instance.Lines[$_.Name]
          Destinations = $_.Group.Name
        }
      }
      [Stop]@{
        StopInfo = $_
        Lines    = $Lines
      }
    }
  }
}