> ## Documentation Index
> Fetch the complete documentation index at: https://docs.camellapp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Matching por score

> Cómo se ordena el pool de camellos candidatos a un trabajo.

Cuando un admin va a asignar un camello a un Job, `GET /api/admin/jobs/:id/candidates` calcula un score por candidato y devuelve el pool ordenado. El score se compone de razones (`reasons`) independientes que se suman; cada razón trae su etiqueta y sus puntos, así que la respuesta explica por qué un candidato quedó donde quedó, no solo el número final.

<Note>
  Esta página documenta el mecanismo tal como está implementado en `src/lib/match-score-shared.ts`. Los pesos son constantes reales del código (`MATCH_WEIGHTS`), no una estimación.
</Note>

## Factores y pesos

| Factor                                   | Peso                                                      | Cuándo aplica                                                                                               |
| ---------------------------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| Misma ciudad                             | 35 puntos                                                 | La ciudad del candidato coincide (normalizada) con la del Job.                                              |
| Mismo barrio (además de la misma ciudad) | +5 puntos                                                 | Además de la misma ciudad, coincide también el barrio/`neighborhood`.                                       |
| Cobertura declarada de municipio         | 20 puntos                                                 | El candidato no está en la misma ciudad, pero declaró ese municipio entre los que cubre (`municipalities`). |
| Fuera de cobertura                       | 0 puntos                                                  | Ni misma ciudad ni municipio declarado.                                                                     |
| Reputación (`rating`)                    | hasta 25 puntos                                           | Media bayesiana del rating, escalada a este peso.                                                           |
| Carga de trabajo (`load`)                | hasta 20 puntos, con penalización de 7 por trabajo activo | A más trabajos activos, menos puntos; nunca baja de 0.                                                      |
| Premium                                  | 15 puntos                                                 | Solo si el candidato es `premium`.                                                                          |

Estos valores son los de la constante `MATCH_WEIGHTS` en `src/lib/match-score-shared.ts`:

```ts theme={null}
export const MATCH_WEIGHTS = {
  geoSameCity: 35,
  geoSameNeighborhood: 5,
  geoCoveredMunicipality: 20,
  rating: 25,
  load: 20,
  loadPenaltyPerJob: 7,
  premium: 15,
  priorRating: 4,
  priorWeight: 3,
} as const;
```

### Geografía

La ciudad y el barrio se comparan normalizados (minúsculas, sin tildes, sin espacios sobrantes) para que variaciones de escritura no rompan la coincidencia. Si el candidato no está en la misma ciudad del Job, se revisa si declaró esa ciudad entre sus `municipalities` (los municipios que él mismo dijo cubrir, no una cobertura inferida por defecto). Si tampoco, la razón de geografía suma 0 puntos pero igual aparece en la respuesta ("Fuera de cobertura").

### Reputación

No se usa el promedio de calificación en bruto: se calcula una **media bayesiana** con un prior de `priorRating: 4` y peso `priorWeight: 3`. La fórmula es:

```text theme={null}
bayes = (reviewCount * ratingAverage + priorWeight * priorRating) / (reviewCount + priorWeight)
```

Sin reseñas, `bayes` cae exactamente en `priorRating` (4 sobre 5): un camello nuevo no queda enterrado por no tener historial, pero tampoco una única reseña de 5 estrellas lo dispara al tope. Esa media bayesiana se escala contra el máximo de rating (5) y se multiplica por el peso `rating` (25): `points = round((bayes / 5) * 25)`.

### Carga

`load` son los trabajos activos del candidato. Los puntos son `max(0, 20 - 7 * load)`: 20 puntos si está libre, 13 con un trabajo activo, 6 con dos, y 0 a partir de tres — nunca negativo.

### Premium

Si el candidato es `premium`, se suma un bono fijo de 15 puntos como una razón adicional (`kind: "premium"`). Si no lo es, esa razón simplemente no aparece en la lista de motivos.

## Desempate

El score es la suma de todas las razones aplicables. Cuando dos candidatos empatan en score, `rankCandidates` desempata en este orden: más reseñas (`reviewCount`) primero, luego orden alfabético del nombre (`localeCompare` en español), y por último el `id` como desempate final y determinista.

## El endpoint

`GET /api/admin/jobs/:id/candidates` devuelve `{ items }`: el pool de candidatos para el servicio de ese Job, ya ordenado por score, con los motivos (`reasons`) de cada puntaje incluidos por candidato. Acepta `limit` de 1 a 50 (default 20) y responde `404` si el trabajo no existe.
