```ts
export function codeOpenAnswers(
  rules: Array<Record<string, unknown>>,
  answers: string[]
): Record<string, unknown> {
  if (!Array.isArray(rules) || rules.length === 0) {
    throw new Error("Rules must be a non-empty list.");
  }

  const ruleMap: { [phrase: string]: string } = {};
  const codes: { [code: string]: number } = {};

  for (const rule of rules) {
    if (
      typeof rule !== "object" ||
      rule === null ||
      typeof rule.code !== "string" ||
      rule.code.trim() === "" ||
      typeof rule.phrase !== "string"
    ) {
      throw new Error("Each rule must be a mapping with 'code' and 'phrase'.");
    }

    const code = rule.code.trim();
    const phrase = rule.phrase.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();

    if (ruleMap[phrase]) {
      throw new Error("No two rules may share the same phrase.");
    }

    if (!/^[a-z0-9 ]+$/.test(phrase)) {
      throw new Error(
        "A phrase must be one or more runs of lowercase letters and digits joined by single spaces."
      );
    }

    ruleMap[phrase] = code;
    codes[code] = 0;
  }

  if (!Array.isArray(answers)) {
    throw new Error("Answers must be a list.");
  }

  const loose: string[] = [];

  for (const answer of answers) {
    if (typeof answer !== "string") {
      throw new Error("Each answer must be a string.");
    }

    const tidiedAnswer = answer
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, " ")
      .trim();

    let coded = false;

    for (const phrase of Object.keys(ruleMap)) {
      if (tidiedAnswer.includes(` ${phrase} `) || 
          (tidiedAnswer.startsWith(phrase) && tidiedAnswer.length === phrase.length) ||
          (tidiedAnswer.startsWith(phrase) && tidiedAnswer[phrase.length] === " ") ||
          (tidiedAnswer.endsWith(phrase) && tidiedAnswer.length === phrase.length) ||
          (tidiedAnswer.endsWith(phrase) && tidiedAnswer[tidiedAnswer.length - phrase.length - 1] === " ")) {
        const code = ruleMap[phrase];
        codes[code]++;
        coded = true;
        break;
      }
    }

    if (!coded) {
      loose.push(tidiedAnswer);
    }
  }

  return {
    tally: Object.entries(codes).map(([code, count]) => ({ code, count })),
    loose,
  };
}
```