DEV Community

Citation Builder
Citation Builder

Posted on

The Google Map Pack Is a Database, Not a Search Result

The Google map pack (the block of three local businesses with a map) is not ranked like the ten blue links below it. Organic results rank documents: pages, scored on content and links. The map pack ranks entities: business records, scored on whether Google trusts the data in the record. Once you internalize that, "why doesn't my client show up in the pack?" stops being an SEO mystery and becomes a debugging problem, and debugging problems are what we're good at.

This post is the engineer's version of that debugging session: how the record is assembled, where it gets corrupted, and how to diff it across sources with a few lines of Node.

The record, not the page

Google's own documentation reduces local ranking to three forces: relevance, distance, and prominence. It also states there is no way to pay for a better local ranking. What it doesn't spell out is where the record's data comes from. Their docs on business information list four inputs:

  • publicly available information, such as crawled web content
  • licensed data from third parties
  • user contributions (suggested edits)
  • Google's own interactions with the place

Read that list again as an engineer: you are not the only writer to this database. Your Google Business Profile is one writer among four. Directories, data aggregators, and random users all hold write access to fields you think you own. And when writers disagree, the merge strategy is not "owner wins", it is "confidence wins": Google may refuse your own edit if it can't corroborate it elsewhere.

That is why two businesses with identical websites can rank completely differently in the pack. One has a record every source agrees on. The other has three phone numbers in the wild.

The eight fields that matter

If the pack is a database contest, these are the columns it sorts on, roughly by how much movement each produces (the full evidence for each is in our map pack ranking factors reference):

# Factor You control it?
1 Primary GBP category Fully
2 Proximity to the searcher No
3 Review volume, recency, rating Indirectly
4 Profile completeness and activity Fully
5 Behavioral signals (calls, directions) Indirectly
6 On-page location relevance Fully
7 Citations and NAP consistency Fully
8 Competitor spam levels Report only

Notice what's fully controllable: the category, the profile data, the landing page, and the consistency of the record across the web. Notice also what most agencies sell hardest (more listings, more links) and where it actually sits in the table.

Diffing a business record across sources

The corruption you're hunting is rarely dramatic. It's "Ste 200" vs "Suite #200", a call-tracking number a previous agency left on one directory, a pre-rebrand name on a listing nobody remembers creating. Each variant is a competing write to the same record.

Here's the minimal diff harness. Every serious directory (and your own site, if you've done it right) exposes the record as LocalBusiness JSON-LD, so you can pull and compare the fields directly:

const norm = {
  phone: (s = '') => s.replace(/[^\d+]/g, '').replace(/^00/, '+'),
  name:  (s = '') => s.toLowerCase().replace(/[^\p{L}\p{N} ]/gu, '').trim(),
  street:(s = '') => s.toLowerCase()
    .replace(/\bsuite\b|\bste\.?\b/g, 'ste')
    .replace(/\bstreet\b|\bst\.?\b/g, 'st')
    .replace(/\s+/g, ' ').trim(),
};

async function record(url) {
  const html = await (await fetch(url)).text();
  const blocks = [...html.matchAll(
    /<script[^>]*application\/ld\+json[^>]*>([\s\S]*?)<\/script>/gi
  )].flatMap(m => { try { return [JSON.parse(m[1])] } catch { return [] } });
  const nodes = blocks.flatMap(b => b['@graph'] ?? [b]);
  const biz = nodes.find(n => /LocalBusiness|Dentist|Plumber|Restaurant/
    .test([].concat(n['@type']).join(',')));
  if (!biz) return null;
  return {
    url,
    name:   norm.name(biz.name),
    phone:  norm.phone(biz.telephone),
    street: norm.street(biz.address?.streetAddress),
  };
}

const sources = await Promise.all(listOfListingUrls.map(record));
const clean = sources.filter(Boolean);
for (const key of ['name', 'phone', 'street']) {
  const variants = new Set(clean.map(r => r[key]));
  if (variants.size > 1)
    console.log(`CONFLICT on ${key}:`, Object.fromEntries(
      clean.map(r => [new URL(r.url).hostname, r[key]])));
}
Enter fullscreen mode Exit fullscreen mode

Run that against a business's known listings and you get the exact output a local SEO audit charges hundreds for: which field disagrees, on which host. The normalization step is where all the real-world pain lives, and it's also the point: if your normalizer needs three rules to match two listings, Google's entity resolution is doing the same work with the same uncertainty, and uncertainty is what suppresses records.

Not every source exposes JSON-LD, and plenty of directories render the NAP with JavaScript, so a fetch-based diff has honest blind spots. For the sources it can't read, tooling should say "unchecked" rather than guess. We run this exact philosophy as a free NAP checker (it reads OpenStreetMap via Nominatim plus two open directories, and it explicitly separates "missing" from "couldn't read"), so you can get the diff without writing the harness.

Fixing writes, not adding rows

The instinct once you find conflicts is to add more listings: more rows must mean more signal. The data says otherwise. Past the point of consistency, bulk listings on low-authority directories add coverage, not rank. What actually moves the record's standing:

  1. Fix the conflicting writes first. Correct or claim the stale listings before creating anything new. Building on top of contradictions scales the contradictions.
  2. Mirror the primary category everywhere. It's the strongest fully-controllable field, and directories have category fields too. Same classification, every source.
  3. Complete the record. Hours, services, attributes. Between two equally close businesses, the more complete record tends to win, and it converts better after it wins.
  4. Then let reviews and behavior compound. They're the heaviest signals you influence only indirectly, and they accumulate on top of a record Google trusts, not instead of it.

The whole discipline, one canonical record enforced across every source that has write access, is called NAP consistency, and it is the least glamorous, highest-floor work in local search. If you'd rather hand the fixing and building to someone whose job it is, that's literally what a citation building service does: audit the existing writes, correct them, create the missing ones, and prove each with the live URL.

The takeaway

Treat the map pack like the database it is. Enumerate the sources that hold your record, diff the fields, fix the writes, and only then optimize the parts everyone obsesses over. A page-one website with a corrupted record loses to a mediocre website with a clean one, every day, in every city.

Top comments (0)