```ts
/**
 * Unspools text that has been shortened using the specified notation.
 * @param spool - The spooled text to be unspooled.
 * @returns The fully expanded text.
 */
export function unspoolText(spool: string): string {
  if (typeof spool !== 'string') {
    throw new Error('Input must be a string');
  }

  let output = '';
  let i = 0;
  const n = spool.length;

  while (i < n) {
    if (spool[i] === '<') {
      if (i + 1 < n && spool[i + 1] === '<') {
        // Double less-than sign: append one less-than sign
        output += '<';
        i += 2;
        continue;
      }

      // Find the position of the greater-than sign
      let gtIndex = i + 1;
      while (gtIndex < n && spool[gtIndex] !== '>') {
        gtIndex++;
      }

      if (gtIndex === n) {
        throw new Error('Pointer must be closed with a greater-than sign');
      }

      // Extract the reach and haul
      const pointer = spool.slice(i + 1, gtIndex);
      const commaIndex = pointer.indexOf(',');

      if (commaIndex === -1) {
        throw new Error('Pointer must contain a comma');
      }

      const reachStr = pointer.slice(0, commaIndex);
      const haulStr = pointer.slice(commaIndex + 1);

      if (!/^\d+$/.test(reachStr) || !/^\d+$/.test(haulStr)) {
        throw new Error('Reach and haul must be positive digits');
      }

      const reach = parseInt(reachStr, 10);
      const haul = parseInt(haulStr, 10);

      if (reach <= 0 || haul <= 0) {
        throw new Error('Reach and haul must be greater than zero');
      }

      if (output.length < reach) {
        throw new Error('Reach cannot exceed the number of characters produced so far');
      }

      // Append the specified characters from the output
      for (let j = 0; j < haul; j++) {
        output += output[output.length - reach];
      }

      i = gtIndex + 1;
    } else {
      // Regular character: append as is
      output += spool[i];
      i++;
    }
  }

  return output;
}
```