```ts
export function spanMerge(spans: number[][]): number[][] {
  if (spans.length === 0) return [];

  const sorted = [...spans].sort((a, b) => a[0] - b[0]);
  const merged: number[][] = [sorted[0]];

  for (let i = 1; i < sorted.length; i++) {
    const [currentStart, currentEnd] = merged[merged.length - 1];
    const [nextStart, nextEnd] = sorted[i];

    if (currentEnd >= nextStart) {
      merged[merged.length - 1] = [currentStart, Math.max(currentEnd, nextEnd)];
    } else {
      merged.push([nextStart, nextEnd]);
    }
  }

  return merged;
}
```

This implementation:
- Handles empty input by returning `[]`.
- Sorts spans by start ascending (then end ascending as a tiebreaker).
- Merges any span whose start is ≤ the current merged span's end (covering both overlap and end-to-start touch cases).
- Uses `Math.max` to extend the current span's end when merging.
- Returns the result ordered by start.
- Does not mutate the input array.