```ts
function whole(value: unknown): boolean {
  return typeof value === "number" && Number.isInteger(value);
}

export function redeemCouponSlips(
  tickets: (string | number)[][],
  slips: (string | number)[][],
  ceiling: number,
): { due: number; saved: number; ignored: string[] } {
  if (!whole(ceiling) || ceiling < 0) {
    throw new Error("the ceiling must be whole and at nought or above");
  }
  if (!Array.isArray(tickets) || !Array.isArray(slips)) {
    throw new Error("tickets and slips must be lists");
  }

  const standing = new Map<string, number>();
  const opening = new Map<string, number>();
  for (const ticket of tickets) {
    if (!Array.isArray(ticket) || ticket.length !== 2) {
      throw new Error("a ticket is a [label, cents] pair");
    }
    const label = ticket[0];
    const cents = ticket[1];
    if (typeof label !== "string" || label.length === 0) {
      throw new Error("a label must be a non-empty string");
    }
    if (standing.has(label)) {
      throw new Error(`two tickets share the label ${label}`);
    }
    if (!whole(cents) || (cents as number) < 0) {
      throw new Error("a price must be whole and at nought or above");
    }
    standing.set(label, cents as number);
    opening.set(label, cents as number);
  }

  const tags = new Set<string>();
  for (const slip of slips) {
    if (!Array.isArray(slip) || slip.length !== 3) {
      throw new Error("a slip is a [tag, label, share] triple");
    }
    const tag = slip[0];
    const label = slip[1];
    const share = slip[2];
    if (typeof tag !== "string" || tag.length === 0) {
      throw new Error("a tag must be a non-empty string");
    }
    if (tags.has(tag)) {
      throw new Error(`two slips share the tag ${tag}`);
    }
    tags.add(tag);
    if (typeof label !== "string" || label.length === 0) {
      throw new Error("a label must be a non-empty string");
    }
    if (!whole(share) || (share as number) < 1 || (share as number) > 100) {
      throw new Error("a share runs from 1 through 100");
    }
  }

  const struck = new Map<string, number>();
  const ignored: string[] = [];
  let saved = 0;
  for (const slip of slips) {
    const tag = slip[0] as string;
    const label = slip[1] as string;
    const share = slip[2] as number;
    if (!standing.has(label) || (struck.get(label) ?? 0) >= 2) {
      ignored.push(tag);
      continue;
    }
    const saving = Math.floor(((opening.get(label) as number) * share) / 100);
    if (saved + saving > ceiling) {
      ignored.push(tag);
      continue;
    }
    standing.set(label, Math.max(0, (standing.get(label) as number) - saving));
    saved += saving;
    struck.set(label, (struck.get(label) ?? 0) + 1);
  }

  let due = 0;
  for (const price of standing.values()) {
    due += price;
  }
  return { due, saved, ignored };
}
```