The Doozer Brainlive from the knowledge bundle · v2.2.618 · 2026-09-13 17:17 ETall pages · one page

Niche matching and live discovery

systems/niche-matching-and-discovery.md · system · nearbymenow, search, places, niche, discovery

status: current

Niche matching and live discovery

How NearByMeNow decides "does this business belong on the <niche> page?" and "what is actually near this person right now?". Three bugs lived here at once, all invisible because each one only ever made results smaller, never wrong.

The rule: our tag OR Google's types

A business carries businesses.primary_niche (our single-value guess) and businesses.types (Google's array). Niche pages matched only the first. That is one guess per business against ~90 niches, so most businesses were unreachable from the page that describes them.

Measured 2026-08-23 on the live network:

niche page matched by primary_niche also tagged by Google
restaurants 82 +361
bars 59 +191
cafes 9 +56
hair salons 14 +19

Use nbmnNicheMatchClause($nicheField, $typesField, $niche, $params, $db), never nbmnNicheInClause directly, for anything a visitor browses. It returns (primary_niche IN (...) OR JSON_CONTAINS(types, ...) OR ...).

Two rules that keep it honest:

  1. establishment and point_of_interest are excluded from the types side.
  2. Google stamps them on 3,393 of 3,635 businesses, so including them makes every niche universal.

  3. Exact primary_niche matches rank FIRST ($nichePrecision in
  4. nbmnFetchListings). Google tags a bowling alley restaurant because it serves food; that is a fine page-3 result and a bad page-1 result.

MariaDB 10.6 - no JSON_OVERLAPS. Use OR'd JSON_CONTAINS(f, JSON_QUOTE(:x), '$').

A niche slug is NOT a Google place type

hair-salons is not hair_salon. auto-repair is not car_repair. plumbers is not plumber. restaurants is not restaurant.

Stripping punctuation out of a URL slug does not produce a place type, and Google silently ignores a type it does not recognise - the search still returns 200 with generic nearby establishments, so the bug looks like "discovery is mediocre" rather than "discovery was never asked the question".

Always resolve through niche_mappings via nbmnNicheTypeValues($niche). When a niche maps to several types, prefer the one matching the slug's singular (hair-salons -> hair_salon, not the broader hair_care).

The thing you asked for is never junk

nbmnJunkTypes() keeps the untyped "who else is on this street" widget free of ATMs, bus stops and gas stations. It contains car_wash, dentist, doctor, pharmacy, hospital, bank, hotel, insurance_agency, real_estate_agency, storage, funeral_home.

It was applied to explicit searches too. A visitor on the car-washes page pressed "Show what's near me", got a correctly typed Google search for car washes, and had every result discarded for being a car wash. Car washes and dentists returned zero cards, every time, for as long as the feature existed.

In nbmnNearbyDiscover, the junk filter runs only when $type === ''. With an explicit type Google has already constrained the set; filtering again can only subtract the answer.

Why all three hid for so long

None of them threw. Each one returned a smaller, plausible result set, and a short list of local businesses looks exactly like a short list of local businesses. The symptom was recorded for months as "thin inventory" and "dead-end niches" - a data problem - when all three were query problems. When a surface looks under-stocked, check what the query is actually asking before concluding the shelf is empty.

The audit (2026-08-24) and what it added

Method: for every path a visitor can take to find a business, compute what the query SHOULD return and compare to what it DOES. Four more defects, same signature - all subtractive, none threw.

niche_mappings.google_type was never validated

It is the question every discovery call asks Google. Some values are not Google types. Google answers 400 INVALID_ARGUMENT, callers turn that into [], and the niche reads "nothing near you" forever while billing for each attempt.

Rejections now land in places_bad_types; dz nichetypes reports them with the niches affected, and fix=1 live=1 applies renames. It applies ONLY renames Google itself rejected - inventing a plausible type would just create a new silent zero. roofer -> roofing_contractor took that niche from 0 results to 18.

A failed call cached its own emptiness for 120 days

An errored lookup and an empty neighbourhood both arrive as []. Both were cached. One bad request could pin a niche to "nothing here" for four months after the cause was fixed. prDiscoveryLastCallFailed() separates them; only a real ZERO_RESULTS is remembered. Never cache a failure as an answer.

"Best match" and "Nearest first" were the same query

The route forced sort=closest whenever an origin was known. Two labels, one order. Best match is now a genuine blend - quality decayed by distance, EXP(-d/8) - which is what a person means by "best" in a local directory. Related: the sort whitelist in the route silently dropped any value not in [closest, rating, reviews], so adding a sort to the UI alone does nothing.

Structural findings (reported, not changed)

The rule this all points at

Every defect here returned a smaller, plausible answer. A short list of local businesses looks exactly like a short list of local businesses, so none of them could be found by using the site - only by counting. Any query that can return fewer results than it should needs a way to say so out loud: record the rejection, flag the fallback, distinguish the error from the empty.

Network Growth — "More places nearby" (build-on-tap, gated)Per-Location Data Enrichment (the true voice of a place)