That's correct - the spatial join is going to give any row from the census blocks that the buildings touch. Instead if you want to guarantee that the building is attached to only a single block then there's a couple of steps.
Let's assume we have some census blocks and some buildings:

fake_census = gpd.GeoDataFrame([
{'block': 'Block A', 'geometry': geometry.box(1, 0, 4, 4)},
{'block': 'Block B', 'geometry': geometry.box(4, 0, 7, 4)}
])
fake_buildings = gpd.GeoDataFrame([
{'building': 'Building 1', 'geometry': geometry.box(1, 0, 2, 1)},
{'building': 'Building 2', 'geometry': geometry.box(3, 1, 6, 3)},
{'building': 'Building 3', 'geometry': geometry.box(0, 3, 2, 5)},
])
- Instead of a spatial join use the
gpd.overlay
function to calculate the spatial intersection between the two layers (and copying across the attributes:
overlay = gpd.overlay(fake_census, fake_buildings)

- Calculate the area of the area and sort descending by the area:
overlay['area'] = overlay.area
overlay.sort_values('area', ascending=False, inplace=True)
| block | building | geometry | area |
|:--------|:-----------|:------------------------------------|-------:|
| Block B | Building 2 | POLYGON ((4 1, 4 3, 6 3, 6 1, 4 1)) | 4 |
| Block A | Building 2 | POLYGON ((4 3, 4 1, 3 1, 3 3, 4 3)) | 2 |
| Block A | Building 1 | POLYGON ((2 0, 1 0, 1 1, 2 1, 2 0)) | 1 |
| Block A | Building 3 | POLYGON ((1 3, 1 4, 2 4, 2 3, 1 3)) | 1 |
- Group by the building identifier and take the
first
row - this will give you the building tied to the census block with the largest area.
overlay.groupby('building').first()
| building | block | geometry | area |
|:-----------|:--------|:------------------------------------|-------:|
| Building 1 | Block A | POLYGON ((2 0, 1 0, 1 1, 2 1, 2 0)) | 1 |
| Building 2 | Block B | POLYGON ((4 1, 4 3, 6 3, 6 1, 4 1)) | 4 |
| Building 3 | Block A | POLYGON ((1 3, 1 4, 2 4, 2 3, 1 3)) | 1 |