<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Avery Callahan</title>
    <description>The latest articles on DEV Community by Avery Callahan (@averycallahan).</description>
    <link>https://dev.to/averycallahan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4241206%2Fbcce6b9e-8b28-42ea-9d76-1071ab9a541f.png</url>
      <title>DEV Community: Avery Callahan</title>
      <link>https://dev.to/averycallahan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/averycallahan"/>
    <language>en</language>
    <item>
      <title>Designing a Local Business Directory That Stays Useful at 72,775 Records</title>
      <dc:creator>Avery Callahan</dc:creator>
      <pubDate>Thu, 12 Nov 2026 13:56:40 +0000</pubDate>
      <link>https://dev.to/averycallahan/designing-a-local-business-directory-that-stays-useful-at-72775-records-4i1g</link>
      <guid>https://dev.to/averycallahan/designing-a-local-business-directory-that-stays-useful-at-72775-records-4i1g</guid>
      <description>&lt;p&gt;A directory has two milestones. The first one is a data milestone: you have N records in a store and they all render. The second is much harder to notice when you hit it. Someone who has never lived in the city they are searching lands on a page, sees three firms they could actually call, and stops looking.&lt;/p&gt;

&lt;p&gt;Property Management Firms is a free US directory of 72,775 property management companies across 10,126 cities and 51 states. Building the browse experience behind it taught me that the interesting engineering is rarely the part that scales the record count. It is the part that keeps a large dataset legible to a stranger in about fifteen seconds. Here are the patterns worth stealing for any local-business directory, at any size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Place is a key, not a substring
&lt;/h2&gt;

&lt;p&gt;The easy schema puts a street address on each firm and moves on. That works right up until you want a city page, and then you discover that your city field is really a free-text remark.&lt;/p&gt;

&lt;p&gt;The fix is to treat place as structured data: a city value, a state value, and a slug for the URL, all derived from one canonical key. Once a city is a key instead of a string, counting firms per city is honest, ranking cities is possible, and the browse tree can be generated instead of hand-maintained. It also gives you a place to put the ugly decisions, like whether a suburb gets its own page or rolls into the metro.&lt;/p&gt;

&lt;p&gt;This matters more in the US than people expect, because place names repeat relentlessly. Springfield is a city in dozens of states. Portland is Oregon and Maine. Kansas City is Missouri and Kansas. If your key is the display name, you will eventually merge two markets that are 1,500 miles apart and never notice until a user complains.&lt;/p&gt;

&lt;h2&gt;
  
  
  Normalize the strings users actually type
&lt;/h2&gt;

&lt;p&gt;The second layer is spelling. Real listings describe the same city as "St. Louis", "Saint Louis", "St Louis" and "SAINT LOUIS". None of those are wrong, and all of them are different strings. Search that depends on exact matching will fail on the most common variant.&lt;/p&gt;

&lt;p&gt;A boring normalization pass handles most of it: lowercase, trim, strip punctuation, expand common abbreviations, and keep the original display value untouched for rendering. You normalize for lookup and index, never for display — nobody wants their city shown as "saint louis".&lt;/p&gt;

&lt;p&gt;The same pass belongs on phone numbers and websites, which bring their own variants: digits with dashes, dots, parentheses, leading country codes, and URLs with or without a trailing slash. Storing a canonical form alongside the displayed one makes deduplication and comparison possible later without a migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small fixed taxonomy beats a big honest one
&lt;/h2&gt;

&lt;p&gt;Firm records describe specialties inconsistently: "apartments", "multi-family", "residential", "community association management", "HOA", "short-term rentals", "vacation rentals", "commercial". Kept as-is, that becomes two hundred near-duplicate tags and a filter sidebar nobody uses.&lt;/p&gt;

&lt;p&gt;Collapsing into a small, fixed set — residential, commercial, apartments, short-term rental and HOA — keeps the filter trustworthy. A firm can hold more than one type, which is the realistic case, and the counts stay comparable because every label means the same thing on every record. The taxonomy is lossy on purpose. You lose the nuance of "community association management" as a phrase and gain the ability to answer "which firms in this city handle HOAs" with a number you can defend.&lt;/p&gt;

&lt;p&gt;Rule of thumb: if a filter value cannot be explained in one clause to someone who is not in the industry, it belongs in the description, not the taxonomy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deduplication is a content decision wearing an engineering hat
&lt;/h2&gt;

&lt;p&gt;At a few thousand records, duplicates are an annoyance. At tens of thousands, they are the main threat to usefulness, and the naive approach makes things worse.&lt;/p&gt;

&lt;p&gt;The same firm appears as "ABC Property Management LLC" and "ABC Property Mgmt". A regional brand appears in three cities with three phone numbers, and all three are legitimate locations. Two unrelated firms share a surname-based brand. Fuzzy name matching will happily merge the third case and split the second, which is exactly backwards from what users need.&lt;/p&gt;

&lt;p&gt;The approach that holds up is to match on the strongest independent signals first — canonical website domain, then normalized phone, then name plus city — and to treat a name-only match as a suggestion rather than a merge. Keep the merge reversible and logged. A wrongly merged record produces a page that looks correct but sends a property owner to the wrong company, which is the worst kind of bug in a directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browse pages carry the traffic, search handles the tail
&lt;/h2&gt;

&lt;p&gt;Most visitors do not type an exact company name. They arrive with a place and a need: a city, a property type, a budget question they think a manager can answer.&lt;/p&gt;

&lt;p&gt;That argues for making the browse surfaces first-class rather than a fallback: state pages, city pages, and property-type pages, all with stable structure and enough firms listed to be worth loading. Search then handles the narrower lookup — the owner who already has a name from a neighbor or a listing.&lt;/p&gt;

&lt;p&gt;Practically, that means the browse layer should be cheap: cached, statically served where possible, and free of per-request work that only search needs. It also means browse pages should never be empty. A city page with one firm is fine. A city page with zero firms and a search box is a dead end, and it is worth suppressing programmatically rather than trusting a crawler to forgive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leave the gate open
&lt;/h2&gt;

&lt;p&gt;The last decision is the cheapest and the easiest to get wrong. Showing a firm's name, rating, phone number and website without requiring an account keeps the directory usable for the person who just wants to call somebody this afternoon.&lt;/p&gt;

&lt;p&gt;Free to browse and search, no signup, is not a growth tactic. It is the product behaving like a reference tool instead of a funnel. Everything that comes later — claiming a listing, comparing firms, saving a shortlist — is optional depth on top of a page that already answers the question.&lt;/p&gt;

&lt;h2&gt;
  
  
  The close
&lt;/h2&gt;

&lt;p&gt;Seventy-two thousand records is not the hard part. The hard part is that a stranger in a city they have never visited can trust a filtered list enough to pick up the phone, and that mostly comes down to place keys, a small taxonomy, conservative deduplication and browse pages that are worth the click.&lt;/p&gt;

&lt;p&gt;Property Management Firms is live at &lt;a href="https://propertymanagementfirms.com/" rel="noopener noreferrer"&gt;https://propertymanagementfirms.com/&lt;/a&gt; if you want to see how the browse tree behaves across 10,126 cities. I write for the directory; the patterns above are the ones I would reuse before adding a single extra feature.&lt;/p&gt;




&lt;p&gt;Note: I write about property management data and the directory tooling on behalf of Property Management Firms.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>architecture</category>
      <category>showdev</category>
      <category>propertymanagement</category>
    </item>
  </channel>
</rss>
