///|
fn index_match_order(left : IndexMatch, right : IndexMatch) -> Int {
  if left.evidence.score > right.evidence.score {
    -1
  } else if left.evidence.score < right.evidence.score {
    1
  } else if left.normalized_name < right.normalized_name {
    -1
  } else if left.normalized_name > right.normalized_name {
    1
  } else if left.record.id < right.record.id {
    -1
  } else if left.record.id > right.record.id {
    1
  } else {
    0
  }
}

///|
fn collect_index_candidates(
  index : NameIndex,
  bucket_keys : Array[String],
) -> Array[String] {
  let candidates : Array[String] = []
  for key in bucket_keys {
    match index.buckets.get(key) {
      None => ()
      Some(ids) =>
        for id in ids {
          if !candidates.contains(id) {
            candidates.push(id)
          }
        }
    }
  }
  candidates
}

///|
/// Retrieves blocking candidates, scores each once, and returns stable top-K results.
pub fn NameIndex::query(
  self : NameIndex,
  name : String,
  limit : Int,
  minimum_score : Double,
) -> Result[IndexQueryResult, IndexError] {
  if limit < 1 {
    return Err(InvalidQueryLimit(limit))
  }
  if minimum_score.is_nan() ||
    minimum_score.is_inf() ||
    minimum_score < 0.0 ||
    minimum_score > 1.0 {
    return Err(InvalidMinimumScore(minimum_score))
  }
  let (normalized_query, bucket_keys) = match
    prepare_index_name(name, self.config, self.blocking_algorithms) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let candidate_ids = collect_index_candidates(self, bucket_keys)
  let matches : Array[IndexMatch] = []
  for id in candidate_ids {
    let record = match self.records.get(id) {
      None => continue
      Some(value) => value
    }
    let normalized_name = match self.normalized.get(id) {
      None => continue
      Some(value) => value
    }
    let evidence = match match_names(name, record.name, self.config) {
      Err(error) => return Err(IndexMatchFailed(error))
      Ok(value) => value
    }
    if evidence.score >= minimum_score {
      matches.push({ record, normalized_name, evidence })
    }
  }
  matches.sort_by(index_match_order)
  if matches.length() > limit {
    matches.truncate(limit)
  }
  Ok({
    query: name,
    normalized_query,
    matches,
    summary: {
      candidate_count: candidate_ids.length(),
      scored_count: candidate_ids.length(),
      returned_count: matches.length(),
    },
  })
}