Skip to content

Commit e3e061d

Browse files
committed
feat(scan): add optional always_allow tier to location_filter
`location_filter` currently checks `block` first and absolutely, so a multi-location posting like "Remote, Belgium or France" is rejected the moment `france` is in `block` — even though Belgium is an acceptable location in the same string. Add an optional `always_allow` list, checked before `block`. A location matching it passes regardless of `block`. Fully backward-compatible: a config with no `always_allow:` key behaves exactly as before (the new const resolves to [] and the guard short-circuits). Refs #650
1 parent d692647 commit e3e061d

2 files changed

Lines changed: 18 additions & 6 deletions

File tree

scan.mjs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,21 +136,25 @@ function buildTitleFilter(titleFilter) {
136136

137137
// ── Location filter ─────────────────────────────────────────────────
138138
// Optional. If `location_filter` is absent from portals.yml, all locations pass.
139-
// Semantics:
139+
// Semantics (case-insensitive substring, in this order):
140140
// - Empty location string → pass (don't penalize missing data)
141-
// - `block` matches → reject (takes precedence over allow)
141+
// - `always_allow` matches → pass (takes precedence over `block` — lets a
142+
// multi-location string like "Remote, Belgium or France" through because
143+
// the home region is an option, even though "france" is blocked)
144+
// - `block` matches → reject
142145
// - `allow` empty → pass (already cleared block)
143146
// - `allow` non-empty → must match at least one keyword
144-
// All matches are case-insensitive substring.
145147

146148
function buildLocationFilter(locationFilter) {
147149
if (!locationFilter) return () => true;
150+
const alwaysAllow = (locationFilter.always_allow || []).map(k => k.toLowerCase());
148151
const allow = (locationFilter.allow || []).map(k => k.toLowerCase());
149152
const block = (locationFilter.block || []).map(k => k.toLowerCase());
150153

151154
return (location) => {
152155
if (!location) return true;
153156
const lower = location.toLowerCase();
157+
if (alwaysAllow.length > 0 && alwaysAllow.some(k => lower.includes(k))) return true;
154158
if (block.length > 0 && block.some(k => lower.includes(k))) return false;
155159
if (allow.length === 0) return true;
156160
return allow.some(k => lower.includes(k));

templates/portals.example.yml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,25 @@
2424
# Filter scanned jobs by location. Applied AFTER title filter, BEFORE dedup.
2525
# If this entire block is absent, all locations pass (current default behavior).
2626
#
27-
# Semantics:
27+
# Semantics (case-insensitive substring, in this order):
2828
# - Empty location string on a job → pass (don't penalize missing data)
29-
# - Any `block` keyword present → reject (takes precedence over allow)
29+
# - Any `always_allow` keyword present → pass (takes precedence over `block`)
30+
# - Any `block` keyword present → reject
3031
# - `allow` empty → pass (already cleared block)
3132
# - `allow` non-empty → must match at least one keyword
32-
# All matches are case-insensitive substring.
33+
#
34+
# `always_allow` is optional. It rescues multi-location postings that name your
35+
# home region: with always_allow ["Belgium"] and block ["France"], a job listed
36+
# "Remote, Belgium or France" passes (Belgium wins), while "Remote, France" is
37+
# still rejected. Omit always_allow entirely and the filter behaves as before.
3338
#
3439
# Example below targets US-based remote + a couple of US metros, blocking
3540
# common foreign hubs. Customize to your geography.
3641

3742
# location_filter:
43+
# always_allow:
44+
# - "Belgium"
45+
# - "Brussels"
3846
# allow:
3947
# - "Remote"
4048
# - "United States"

0 commit comments

Comments
 (0)