# How the Location Filter Algorithm Handles always_allow, block, and allow Tiers in Career-Ops

> Understand how the Career-Ops location filter algorithm prioritizes always_allow block and allow tiers to precisely manage job location access.

- Repository: [Santiago Fernández de Valderrama/career-ops](https://github.com/santifer/career-ops)
- Tags: deep-dive
- Published: 2026-06-07

---

**The location filter algorithm processes three keyword tiers in strict precedence order—always_allow overrides block, which overrides allow—to determine whether a job location passes the filter in santifer/career-ops.**

The **location filter algorithm** in the `santifer/career-ops` repository enables precise geographical filtering of job postings through a three-tiered matching system. Implemented in `scan.mjs`, the `buildLocationFilter` function normalizes user-defined keywords and applies precedence rules that guarantee certain locations always pass while others are explicitly blocked or conditionally allowed.

## Understanding the Three-Tier Precedence System

The algorithm evaluates locations against three distinct keyword lists in a specific hierarchy. This design ensures that high-priority allowances cannot be accidentally blocked by broader exclusion rules.

### Tier 1: always_allow (Highest Priority)

The **always_allow** tier acts as an unconditional pass list. If a location string contains any keyword from this list, the filter immediately returns `true`, bypassing both the `block` and `allow` tiers entirely. This mechanism ensures that multi-location strings such as "Remote, Belgium or France" pass through when "remote" is listed in `always_allow`, regardless of other geographical restrictions.

### Tier 2: block (Negative Filtering)

The **block** tier functions as a blacklist, but only when no `always_allow` match exists. If the filter reaches this stage and finds any `block` keyword present in the location, it rejects the location by returning `false`. This tier cannot override entries in the `always_allow` list.

### Tier 3: allow (Whitelist Requirement)

The **allow** tier serves as a final whitelist filter. When this list contains keywords, the location must include at least one of them to pass. However, if the `allow` list is empty after normalization, the filter interprets this as "no additional restrictions" and automatically passes any location that has successfully cleared the previous tiers.

## Input Normalization in scan.mjs

Before matching begins, the `normalizeKeywordList` helper (lines 51-58 in `scan.mjs`) sanitizes each tier's input to ensure consistent substring matching:

- Converts `null` or `undefined` to empty arrays
- Flattens single strings into one-item arrays  
- Discards non-string entries, lower-cases remaining values, trims whitespace, and removes empty strings

This normalization guarantees that the subsequent logic in `buildLocationFilter` operates on clean, lowercase string arrays suitable for substring searching.

## Implementation Details

The core logic resides in the arrow function returned by `buildLocationFilter` (lines 66-73), which implements the precedence rules documented in the comment block at lines 33-44:

```javascript
return (location) => {
  if (typeof location !== 'string' || location.trim() === '') return true;
  const lower = location.toLowerCase();
  if (alwaysAllow.length > 0 && alwaysAllow.some(k => lower.includes(k))) return true;
  if (block.length > 0 && block.some(k => lower.includes(k))) return false;
  if (allow.length === 0) return true;
  return allow.some(k => lower.includes(k));
};

```

**Non-string or empty locations** automatically pass (`true`) at line 67 to avoid penalizing malformed provider data. The function then checks tiers in sequence: `alwaysAllow` (lines 68-69), `block` (line 70), and finally `allow` (lines 71-72).

## Practical Configuration Examples

Demonstrating tier precedence with real-world scenarios:

```javascript
import { buildLocationFilter } from './scan.mjs';

const config = {
  location_filter: {
    always_allow: ['remote'],
    block: ['france', 'germany'],
    allow: ['europe', 'asia']
  }
};

const filter = buildLocationFilter(config.location_filter);

// Precedence demonstrations
console.log(filter('Remote, Belgium'));   // true  – matches always_allow
console.log(filter('Paris, France'));     // false – blocked (no always_allow match)
console.log(filter('Remote, Germany'));   // true  – always_allow beats block
console.log(filter('Tokyo, Japan'));      // false – not in allow list
console.log(filter('London, Europe'));    // true  – matches allow list

```

When the `allow` tier is omitted, any non-blocked location passes:

```javascript
const cfgNoAllow = {
  location_filter: {
    always_allow: ['remote'],
    block: ['france']
    // allow omitted → [] internally
  }
};

const filterNoAllow = buildLocationFilter(cfgNoAllow.location_filter);
console.log(filterNoAllow('Paris, France'));   // false – blocked
console.log(filterNoAllow('Madrid, Spain'));   // true  – passes (no allow restriction)

```

## Summary

- The **location filter algorithm** enforces a strict **always_allow → block → allow** precedence order defined in `scan.mjs`
- **always_allow** keywords bypass all other restrictions immediately, making them ideal for "Remote" or "Global" listings that override geographical blocks
- The **block** tier acts as a blacklist only when no always_allow match exists
- An empty **allow** list permits all non-blocked locations, while a populated list requires explicit substring matches
- All inputs pass through `normalizeKeywordList` to ensure consistent lowercase substring matching via `String.prototype.includes()`

## Frequently Asked Questions

### What happens if a location matches both always_allow and block?

The **always_allow** tier takes absolute precedence. If a location contains any keyword from the always_allow list (such as "remote"), the filter returns `true` immediately at line 68 of `scan.mjs` and never evaluates the block tier. This guarantees that explicitly allowed location types can never be blocked by geographical restrictions.

### How does the algorithm handle null or undefined keyword lists?

The `normalizeKeywordList` function converts `null` or `undefined` inputs into empty arrays during the initialization phase (lines 51-58 of `scan.mjs`). This normalization allows the filter logic to treat missing configurations as non-restrictive tiers that safely skip their respective validation checks.

### Why does an empty allow list pass all locations?

When the `allow` array is empty after normalization, the filter encounters line 71 (`if (allow.length === 0) return true;`), which interprets an empty whitelist as "no additional restrictions required." This design pattern enables users to create block-only filters (excluding specific countries) without needing to enumerate every allowed region explicitly.

### Does the filter perform exact string matching or substring matching?

The algorithm uses **substring matching** via `String.prototype.includes()`. Each normalized keyword is checked as a substring within the lower-cased location string, enabling partial matches like "remote" within "Remote, Belgium" without requiring exact equality or word-boundary matching.